Compare commits

...

532 Commits

Author SHA1 Message Date
RiverRay
3c859fc29f Update README.md 2024-12-23 22:47:16 +08:00
Dogtiti
1d15666713 Merge pull request #5919 from Yiming3/feature/flexible-visual-model
feat: runtime configuration of vision-capable models
2024-12-22 10:37:57 +08:00
Yiming Zhang
a127ae1fb4 docs: add VISION_MODELS section to README files 2024-12-21 13:12:41 -05:00
Yiming Zhang
ea1329f73e fix: add optional chaining to prevent errors when accessing visionModels 2024-12-21 04:07:58 -05:00
Yiming Zhang
149d732cb7 Merge remote-tracking branch 'upstream/main' into feature/flexible-visual-model 2024-12-21 03:53:05 -05:00
Yiming Zhang
210b29bfbe refactor: remove NEXT_PUBLIC_ prefix from VISION_MODELS env var 2024-12-21 03:51:54 -05:00
Dogtiti
acc2e97aab Merge pull request #5959 from dupl/gemini
add gemini-exp-1206, gemini-2.0-flash-thinking-exp-1219
2024-12-21 16:30:09 +08:00
dupl
93ac0e5017 Reorganized the Gemini model 2024-12-21 15:26:33 +08:00
Yiming Zhang
ed8c3580c8 test: add unit tests for isVisionModel utility function 2024-12-20 19:07:00 -05:00
dupl
0a056a7c5c add gemini-exp-1206, gemini-2.0-flash-thinking-exp-1219 2024-12-21 08:00:37 +08:00
Yiming Zhang
74c4711cdd Merge remote-tracking branch 'upstream/main' into feature/flexible-visual-model 2024-12-20 18:34:07 -05:00
Dogtiti
eceec092cf Merge pull request #5932 from fengzai6/update-google-models
Update google models to add gemini-2.0
2024-12-21 00:43:02 +08:00
Dogtiti
42743410a8 Merge pull request #5940 from ChatGPTNextWeb/dependabot/npm_and_yarn/testing-library/react-16.1.0
chore(deps-dev): bump @testing-library/react from 16.0.1 to 16.1.0
2024-12-21 00:41:45 +08:00
Dogtiti
0f04756d4c Merge pull request #5936 from InitialXKO/main
面具“以文搜图”改成“AI文生图”,微调提示让图片生成更稳定无水印
2024-12-21 00:40:45 +08:00
dependabot[bot]
acdded8161 chore(deps-dev): bump @testing-library/react from 16.0.1 to 16.1.0
Bumps [@testing-library/react](https://github.com/testing-library/react-testing-library) from 16.0.1 to 16.1.0.
- [Release notes](https://github.com/testing-library/react-testing-library/releases)
- [Changelog](https://github.com/testing-library/react-testing-library/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/react-testing-library/compare/v16.0.1...v16.1.0)

---
updated-dependencies:
- dependency-name: "@testing-library/react"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-16 10:57:34 +00:00
InitialXKO
e939ce5a02 面具“以文搜图”改成“AI文生图”,微调提示让图片生成更稳定无水印 2024-12-13 22:29:14 +08:00
Nacho.L
46a0b100f7 Update versionKeywords 2024-12-13 08:29:43 +08:00
Nacho.L
e27e8fb0e1 Update google models 2024-12-13 07:22:16 +08:00
Yiming Zhang
a433d1606c feat: use regex patterns for vision models and allow adding capabilities to models through env var NEXT_PUBLIC_VISION_MODELS. 2024-12-10 00:22:45 -05:00
Dogtiti
83cea3a90d Merge pull request #5879 from frostime/textline-custom-model
🎨 style(setting): Place custom-model's input a separated row.
2024-11-28 12:02:42 +08:00
frostime
759a09a76c 🎨 style(setting): Place custom-model's input a seperated row. 2024-11-27 13:11:18 +08:00
Dogtiti
2623a92763 Merge pull request #5850 from code-october/fix-o1
Fix o1
2024-11-25 12:31:36 +08:00
Dogtiti
3932c594c7 Merge pull request #5861 from code-october/update-model
update new model for gpt-4o and gemini-exp
2024-11-22 20:59:30 +08:00
code-october
b7acb89096 update new model for gpt-4o and gemini-exp 2024-11-22 09:48:50 +00:00
code-october
ef24d3e633 use stream when request o1 2024-11-21 03:46:10 +00:00
code-october
23350c842b fix o1 in disableGPT4 2024-11-21 03:45:07 +00:00
Dogtiti
a2adfbbd32 Merge pull request #5821 from Sherlocksuper/scroll
feat: support more user-friendly scrolling
2024-11-16 15:24:46 +08:00
Lloyd Zhou
f22cec1eb4 Merge pull request #5827 from ConnectAI-E/fix/markdown-embed-codeblock
fix: 代码块嵌入小代码块时渲染错误
2024-11-15 16:03:27 +08:00
opchips
e56216549e fix: 代码块嵌入小代码块时渲染错误 2024-11-15 11:56:26 +08:00
Sherlock
19facc7c85 feat: support mort user-friendly scrolling 2024-11-14 21:31:45 +08:00
Lloyd Zhou
b08ce5630c Merge pull request #5819 from ConnectAI-E/fix-gemini-summary
Fix gemini summary
2024-11-13 15:17:44 +08:00
DDMeaqua
b41c012d27 chore: shouldStream 2024-11-13 15:12:46 +08:00
Lloyd Zhou
a392daab71 Merge pull request #5816 from ConnectAI-E/feature/artifacts-svg
artifacts support svg
2024-11-13 14:58:33 +08:00
DDMeaqua
0628ddfc6f chore: update 2024-11-13 14:27:41 +08:00
DDMeaqua
7eda14f138 fix: [#5308] gemini对话总结 2024-11-13 14:24:44 +08:00
opchips
9a86c42c95 update 2024-11-12 16:33:55 +08:00
Lloyd Zhou
819d249a09 Merge pull request #5815 from LovelyGuYiMeng/main
更新视觉模型匹配关键词
2024-11-12 15:04:11 +08:00
LovelyGuYiMeng
8d66fedb1f Update visionKeywords 2024-11-12 14:28:11 +08:00
Lloyd Zhou
7cf89b53ce Merge pull request #5812 from ConnectAI-E/fix/rerender-chat
fix: use current session id to trigger rerender
2024-11-12 13:49:51 +08:00
Dogtiti
459c373f13 Merge pull request #5807 from ChatGPTNextWeb/dependabot/npm_and_yarn/testing-library/jest-dom-6.6.3
chore(deps-dev): bump @testing-library/jest-dom from 6.6.2 to 6.6.3
2024-11-11 20:59:56 +08:00
Dogtiti
1d14a991ee fix: use current session id to trigger rerender 2024-11-11 20:30:59 +08:00
dependabot[bot]
05ef5adfa7 chore(deps-dev): bump @testing-library/jest-dom from 6.6.2 to 6.6.3
Bumps [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) from 6.6.2 to 6.6.3.
- [Release notes](https://github.com/testing-library/jest-dom/releases)
- [Changelog](https://github.com/testing-library/jest-dom/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/jest-dom/compare/v6.6.2...v6.6.3)

---
updated-dependencies:
- dependency-name: "@testing-library/jest-dom"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-11 10:53:00 +00:00
lloydzhou
38fa3056df update version 2024-11-11 13:26:08 +08:00
Lloyd Zhou
289aeec8af Merge pull request #5786 from ConnectAI-E/feature/realtime-chat
Feature/realtime chat
2024-11-11 13:19:26 +08:00
lloydzhou
7d71da938f remove close-24 svg 2024-11-11 13:15:09 +08:00
Lloyd Zhou
f8f6954115 Merge pull request #5779 from ConnectAI-E/feature/model/claude35haiku
add claude35haiku & not support vision
2024-11-11 13:13:09 +08:00
Lloyd Zhou
6e03f32871 Merge pull request #5795 from JingSyue/main
fix: built-in plugin dalle3 error #5787
2024-11-11 13:10:00 +08:00
JingSyue
18a6571883 Update proxy.ts
Update proxy.ts
2024-11-11 12:59:29 +08:00
Dogtiti
14f444e1f0 doc: realtime chat 2024-11-11 11:47:41 +08:00
JingSyue
2b0f2e5f9d fix: built-in plugin dalle3 error #5787 2024-11-10 10:28:25 +08:00
Dogtiti
4629b39c29 chore: comment context history 2024-11-09 16:22:01 +08:00
Dogtiti
d33e772fa5 feat: voice print 2024-11-08 22:39:17 +08:00
Dogtiti
89136fba32 feat: voice print 2024-11-08 22:18:39 +08:00
Dogtiti
8b4ca133fd feat: voice print 2024-11-08 22:02:31 +08:00
lloydzhou
a4c9eaf6cd do not save empty audio file 2024-11-08 13:43:13 +08:00
lloydzhou
50e63109a3 merge code and get analyser data 2024-11-08 13:21:40 +08:00
Dogtiti
48a1e8a584 chore: i18n 2024-11-07 21:32:47 +08:00
Dogtiti
e44ebe3f0e feat: realtime config 2024-11-07 21:28:23 +08:00
Lloyd Zhou
108069a0c6 Merge pull request #5788 from ConnectAI-E/fix-o1-maxtokens
chore: o1模型使用max_completion_tokens
2024-11-07 20:06:30 +08:00
DDMeaqua
d5bda2904d chore: o1模型使用max_completion_tokens 2024-11-07 19:45:27 +08:00
lloydzhou
283caba8ce stop streaming play after get input audio. 2024-11-07 18:57:57 +08:00
lloydzhou
b78e5db817 add temperature config 2024-11-07 17:55:51 +08:00
lloydzhou
46c469b2d7 add voice config 2024-11-07 17:47:55 +08:00
lloydzhou
c00ebbea4f update 2024-11-07 17:40:03 +08:00
lloydzhou
c526ff80b5 update 2024-11-07 17:23:20 +08:00
lloydzhou
0037b0c944 ts error 2024-11-07 17:03:04 +08:00
lloydzhou
6f81bb3b8a add context after connected 2024-11-07 16:56:15 +08:00
lloydzhou
7bdc45ed3e connect realtime model when open panel 2024-11-07 16:41:24 +08:00
Dogtiti
88cd3ac122 fix: ts error 2024-11-07 12:16:11 +08:00
Dogtiti
4988d2ee26 fix: ts error 2024-11-07 11:56:58 +08:00
lloydzhou
8deb7a92ee hotfix for update target session 2024-11-07 11:53:01 +08:00
lloydzhou
db060d732a upload save record wav file 2024-11-07 11:45:38 +08:00
lloydzhou
522627820a upload save wav file logic 2024-11-07 09:36:22 +08:00
lloydzhou
cf46d5ad63 upload response audio, and update audio_url to session message 2024-11-07 01:12:08 +08:00
Dogtiti
a4941521d0 feat: audio to message 2024-11-06 22:30:02 +08:00
Dogtiti
f6e1f8398b wip 2024-11-06 22:07:33 +08:00
Dogtiti
d544eead38 feat: realtime chat ui 2024-11-06 21:14:45 +08:00
Lloyd Zhou
fbb9385f23 Merge pull request #5782 from ConnectAI-E/style/classname
style: improve classname by clsx
2024-11-06 20:33:51 +08:00
Dogtiti
18144c3d9c chore: clsx 2024-11-06 20:16:38 +08:00
opchips
64aa760e58 update claude rank 2024-11-06 19:18:05 +08:00
Dogtiti
e0bbb8bb68 style: improve classname by clsx 2024-11-06 16:58:26 +08:00
opchips
6667ee1c7f merge main 2024-11-06 15:08:18 +08:00
Lloyd Zhou
6ded4e96e7 Merge pull request #5778 from ConnectAI-E/fix/5436
fix: botMessage reply date
2024-11-06 15:04:46 +08:00
Dogtiti
85cdcab850 fix: botMessage reply date 2024-11-06 14:53:08 +08:00
Lloyd Zhou
f4c9410c29 Merge pull request #5776 from ConnectAI-E/feat-glm
fix: glm chatpath
2024-11-06 14:02:20 +08:00
DDMeaqua
adf7d8200b fix: glm chatpath 2024-11-06 13:55:57 +08:00
opchips
3086a2fa77 add claude35haiku not vision 2024-11-06 12:56:24 +08:00
Lloyd Zhou
f526d6f560 Merge pull request #5774 from ConnectAI-E/feature/update-target-session
fix: updateCurrentSession => updateTargetSession
2024-11-06 11:16:33 +08:00
Dogtiti
106461a1e7 Merge branch 'main' of https://github.com/ConnectAI-E/ChatGPT-Next-Web into feature/update-target-session 2024-11-06 11:08:41 +08:00
Dogtiti
c4e19dbc59 fix: updateCurrentSession => updateTargetSession 2024-11-06 11:06:18 +08:00
Dogtiti
f3603e59fa Merge pull request #5769 from ryanhex53/fix-model-multi@
Custom model names can include the `@` symbol by itself.
2024-11-06 10:49:28 +08:00
ryanhex53
8e2484fcdf Refactor: Replace all provider split occurrences with getModelProvider utility method 2024-11-05 13:52:54 +00:00
lloydzhou
00d6cb27f7 update version 2024-11-05 17:42:55 +08:00
ryanhex53
b844045d23 Custom model names can include the @ symbol by itself.
To specify the model's provider, append it after the model name using `@` as before.

This format supports cases like `google vertex ai` with a model name like `claude-3-5-sonnet@20240620`.

For instance, `claude-3-5-sonnet@20240620@vertex-ai` will be split by `split(/@(?!.*@)/)` into:

`[ 'claude-3-5-sonnet@20240620', 'vertex-ai' ]`, where the former is the model name and the latter is the custom provider.
2024-11-05 07:44:12 +00:00
Lloyd Zhou
e49fe976d9 Merge pull request #5765 from ConnectAI-E/feature/onfinish
feat: update real 'currentSession'
2024-11-05 15:07:52 +08:00
Dogtiti
14f751965f Merge pull request #5767 from ConnectAI-E/feat-glm
chore: update readme
2024-11-05 11:07:52 +08:00
DDMeaqua
0ec423389f chore: update readme 2024-11-05 11:06:20 +08:00
Dogtiti
820ab54e2d Merge pull request #5766 from ConnectAI-E/feature/add-claude-haiku3.5
Feature/add claude haiku3.5
2024-11-05 10:54:52 +08:00
lloydzhou
a6c1eb27a8 add claude 3.5 haiku 2024-11-05 10:23:15 +08:00
Lloyd Zhou
0dc4071ccc Merge pull request #5464 from endless-learner/main
Added 1-click deployment link for Alibaba Cloud.
2024-11-05 01:10:06 +08:00
Lloyd Zhou
4d3949718a merge main 2024-11-05 01:09:27 +08:00
Dogtiti
aef535f1a7 Merge pull request #5753 from ChatGPTNextWeb/feat-bt-doc
Feat bt doc
2024-11-04 21:41:11 +08:00
Dogtiti
686a80e727 Merge pull request #5764 from ChatGPTNextWeb/dependabot/npm_and_yarn/testing-library/react-16.0.1
chore(deps-dev): bump @testing-library/react from 16.0.0 to 16.0.1
2024-11-04 21:37:34 +08:00
Dogtiti
e49466fa05 feat: update real 'currentSession' 2024-11-04 21:25:56 +08:00
dependabot[bot]
4b93370814 chore(deps-dev): bump @testing-library/react from 16.0.0 to 16.0.1
Bumps [@testing-library/react](https://github.com/testing-library/react-testing-library) from 16.0.0 to 16.0.1.
- [Release notes](https://github.com/testing-library/react-testing-library/releases)
- [Changelog](https://github.com/testing-library/react-testing-library/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/react-testing-library/compare/v16.0.0...v16.0.1)

---
updated-dependencies:
- dependency-name: "@testing-library/react"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-04 10:24:30 +00:00
Dogtiti
5733e3c588 Merge pull request #5759 from ConnectAI-E/feature/onfinish
Feature/onfinish
2024-11-04 17:16:44 +08:00
Dogtiti
44fc5b5cbf fix: onfinish responseRes 2024-11-04 17:00:45 +08:00
Dogtiti
2d3f7c922f fix: vision model dalle3 2024-11-04 15:51:04 +08:00
GH Action - Upstream Sync
fe8cca3730 Merge branch 'main' of https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web 2024-11-02 01:12:09 +00:00
weige
fbb7a1e853 fix 2024-11-01 18:20:16 +08:00
weige
fb2c15567d fix 2024-11-01 17:45:50 +08:00
weige
c2c52a1f60 fix 2024-11-01 17:35:34 +08:00
weige
106ddc17cd fix 2024-11-01 17:35:09 +08:00
weige
17d5209738 add bt install doc 2024-11-01 17:28:20 +08:00
Dogtiti
d66bfc6352 Merge pull request #5752 from ConnectAI-E/feat-glm
fix: ts error
2024-11-01 14:16:50 +08:00
DDMeaqua
4d75b23ed1 fix: ts error 2024-11-01 14:15:12 +08:00
Dogtiti
36bfa2ef7c Merge pull request #5741 from ConnectAI-E/feat-glm
feat: [#5714] 支持GLM
2024-11-01 13:57:30 +08:00
DDMeaqua
afe12c212e chore: update 2024-11-01 13:53:43 +08:00
GH Action - Upstream Sync
adf97c6d8b Merge branch 'main' of https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web 2024-11-01 01:18:59 +00:00
DDMeaqua
7a8d557ea3 chore: 开启插件 2024-10-31 11:37:19 +08:00
DDMeaqua
d3f0a77830 chore: update Provider 2024-10-31 11:23:06 +08:00
Dogtiti
0581e37236 Merge pull request #5744 from mrcore/main
add  claude-3-5-sonnet-latest and claude-3-opus-latest
2024-10-31 11:19:34 +08:00
Core
44383a8b33 add claude-3-5-sonnet-latest and claude-3-opus-latest
add  claude-3-5-sonnet-latest and claude-3-opus-latest
2024-10-31 11:00:45 +08:00
GH Action - Upstream Sync
7c466c9b9c Merge branch 'main' of https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web 2024-10-31 01:14:28 +00:00
Dogtiti
a0fa4d7e72 Merge pull request #5737 from hyiip/claude3.5
add constant to claude 3.5 sonnet 20241022
2024-10-31 00:13:16 +08:00
DDMeaqua
d357b45e84 feat: [#5714] 支持GLM 2024-10-30 19:24:03 +08:00
Lloyd Zhou
d0bd1bf8fd Merge pull request #5740 from yuxuan-ctrl/main
feat: 新增阿里系模型代码配置
2024-10-30 16:56:53 +08:00
yuxuan-ctrl
86ffa1e643 feat: 新增阿里系模型代码配置 2024-10-30 16:30:01 +08:00
endless-learner
b0d28eb77e Merge branch 'main' into main 2024-10-29 14:38:49 -07:00
hyiip
736cbdbdd1 add constant to claude 3.5 sonnet 20241022 2024-10-30 02:18:41 +08:00
Dogtiti
613d67eada Merge pull request #5729 from ConnectAI-E/feature/jest
chore: improve jest
2024-10-29 19:39:59 +08:00
Dogtiti
89cea18955 Merge branch 'main' of https://github.com/ConnectAI-E/ChatGPT-Next-Web into feature/jest 2024-10-29 19:26:52 +08:00
Dogtiti
56bc77d20b Merge pull request #5731 from ChatGPTNextWeb/dependabot/npm_and_yarn/testing-library/jest-dom-6.6.2
Bump @testing-library/jest-dom from 6.4.8 to 6.6.2
2024-10-28 21:52:08 +08:00
Dogtiti
6d93d37963 Merge pull request #5732 from ChatGPTNextWeb/dependabot/npm_and_yarn/types/jest-29.5.14
Bump @types/jest from 29.5.13 to 29.5.14
2024-10-28 21:51:59 +08:00
dependabot[bot]
24df85cf9d Bump @types/jest from 29.5.13 to 29.5.14
Bumps [@types/jest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jest) from 29.5.13 to 29.5.14.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jest)

---
updated-dependencies:
- dependency-name: "@types/jest"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-28 10:31:34 +00:00
dependabot[bot]
a4d7a2c6e3 Bump @testing-library/jest-dom from 6.4.8 to 6.6.2
Bumps [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) from 6.4.8 to 6.6.2.
- [Release notes](https://github.com/testing-library/jest-dom/releases)
- [Changelog](https://github.com/testing-library/jest-dom/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/jest-dom/compare/v6.4.8...v6.6.2)

---
updated-dependencies:
- dependency-name: "@testing-library/jest-dom"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-28 10:31:27 +00:00
Dogtiti
49d42bb45d chore: improve jest 2024-10-28 16:47:05 +08:00
Lloyd Zhou
4f49626303 Merge pull request #5722 from ElricLiu/main
Update README.md
2024-10-26 12:09:09 +08:00
ElricLiu
45db20c1c3 Update README.md 2024-10-26 11:16:43 +08:00
Lloyd Zhou
82994843f5 Merge pull request #5719 from ConnectAI-E/hotfix/status_text_error
hotfix for statusText is non ISO-8859-1 #5717
2024-10-25 20:34:15 +08:00
Dogtiti
1110a087a0 Merge pull request #5720 from ConnectAI-E/hotfix/gemini_invald_argument
hotfix for gemini invald argument #5715
2024-10-25 18:25:46 +08:00
lloydzhou
f0b3e10a6c hotfix for gemini invald argument #5715 2024-10-25 18:19:22 +08:00
lloydzhou
f89872b833 hotfix for gemini invald argument #5715 2024-10-25 18:12:09 +08:00
lloydzhou
90ced92876 update 2024-10-25 18:05:29 +08:00
lloydzhou
2c74559010 hitfix 2024-10-25 18:02:51 +08:00
lloydzhou
e3ca7e8b44 hotfix for statusText is non ISO-8859-1 #5717 2024-10-25 17:52:08 +08:00
lloydzhou
4745706c42 update version to v2.15.6 2024-10-24 15:32:27 +08:00
lloydzhou
801dc412f9 add claude-3.5-haiku 2024-10-24 15:28:05 +08:00
Dogtiti
c7c2c0211a Merge pull request #5704 from ConnectAI-E/feature/xai
xAi support
2024-10-23 14:13:17 +08:00
lloydzhou
65bb962fc0 hotfix 2024-10-23 12:00:59 +08:00
lloydzhou
e791cd441d add xai 2024-10-23 11:55:25 +08:00
lloydzhou
8455fefc8a add xai 2024-10-23 11:40:06 +08:00
Lloyd Zhou
06f897f32f Merge pull request #5679 from ConnectAI-E/fix/fetch
fix: use tauri fetch
2024-10-16 22:02:16 +08:00
Dogtiti
deb1e76c41 fix: use tauri fetch 2024-10-16 21:57:07 +08:00
lloydzhou
463fa743e9 update version 2024-10-15 16:10:44 +08:00
Dogtiti
cda4494cec Merge pull request #5632 from ConnectAI-E/feature/H0llyW00dzZ-updater
Feature/h0lly w00dz z updater
2024-10-15 14:31:49 +08:00
lloydzhou
87d85c10c3 update 2024-10-14 21:48:36 +08:00
Dogtiti
22f83c9e11 Merge pull request #5666 from ChatGPTNextWeb/dependabot/npm_and_yarn/types/jest-29.5.13
Bump @types/jest from 29.5.12 to 29.5.13
2024-10-14 20:36:53 +08:00
dependabot[bot]
7f454cbcec Bump @types/jest from 29.5.12 to 29.5.13
Bumps [@types/jest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jest) from 29.5.12 to 29.5.13.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jest)

---
updated-dependencies:
- dependency-name: "@types/jest"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-14 10:49:46 +00:00
lloydzhou
426269d795 Merge remote-tracking branch 'connectai/main' into feature/H0llyW00dzZ-updater 2024-10-14 17:12:08 +08:00
Lloyd Zhou
370f143157 Merge pull request #5661 from ChatGPTNextWeb/remove-pr-preview
update test run target
2024-10-14 17:11:26 +08:00
lloydzhou
103106bb93 update test run target 2024-10-14 17:10:02 +08:00
lloydzhou
2419083adf Merge remote-tracking branch 'connectai/main' into feature/H0llyW00dzZ-updater 2024-10-14 17:04:12 +08:00
Lloyd Zhou
c25903bfb4 Merge pull request #5658 from ccq18/main
fix  o1系列模型超时时间改为4分钟,
2024-10-14 16:57:29 +08:00
Lloyd Zhou
e34c266438 Merge pull request #5660 from ChatGPTNextWeb/remove-pr-preview
update deploy_preview run target
2024-10-14 16:55:48 +08:00
lloydzhou
8c39a687b5 update deploy_preview run target 2024-10-14 16:53:46 +08:00
ccq18
592f62005b 仅修改o1的超时时间为4分钟,减少o1系列模型请求失败的情况 2024-10-14 16:31:17 +08:00
ccq18
12e7caa209 fix 默认超时时间改为3分钟,支持o1-mini 2024-10-14 16:03:01 +08:00
Lloyd Zhou
b016771555 Merge pull request #5599 from ConnectAI-E/feature/allow-send-image-only
feat: allow send image only
2024-10-14 15:11:28 +08:00
Dogtiti
a84383f919 Merge pull request #5647 from code-october/fix/setting-locale
修改“压缩模型”名称,增加“生成对话标题”的功能提示
2024-10-13 01:49:51 +08:00
code-october
7f68fb1ff2 修改“压缩模型”名称,增加“生成对话标题”的功能提示 2024-10-12 16:49:24 +00:00
Dogtiti
8d2003fe68 Merge pull request #5644 from ConnectAI-E/fix/siderbar-style
fix: sidebar style
2024-10-12 14:56:01 +08:00
Dogtiti
9961b513cc fix: sidebar style 2024-10-12 14:54:22 +08:00
Dogtiti
819238acaf fix: i18n 2024-10-11 20:49:43 +08:00
Dogtiti
ad49916b1c Merge pull request #5638 from ConnectAI-E/chore/test-action
chore: improve test
2024-10-11 20:44:20 +08:00
Dogtiti
d18bd8a48a Merge pull request #5640 from code-october/feature/enableCodeFold
支持前端使能/禁用代码折叠
2024-10-11 20:43:43 +08:00
code-october
4a1319f2c0 代码安全优化 2024-10-11 11:57:23 +00:00
code-october
8fd843d228 参考coderabbitai建议规范代码 2024-10-11 11:38:52 +00:00
code-october
6792d6e475 支持前端使能/禁用代码折叠 2024-10-11 11:19:36 +00:00
Lloyd Zhou
c139038e01 Merge pull request #5639 from code-october/fix/auth-ui
优化访问码输入框
2024-10-11 19:11:35 +08:00
code-october
4a7fd3a380 优化首页 api 输入框 2024-10-11 10:36:11 +00:00
code-october
c98dc31cdf 优化访问码输入框 2024-10-11 09:03:20 +00:00
Dogtiti
bd43af3a8d chore: cache node_modules 2024-10-11 15:41:46 +08:00
Dogtiti
be98aa2078 chore: improve test 2024-10-11 15:17:38 +08:00
lloydzhou
a0d4a04192 update 2024-10-11 11:52:24 +08:00
lloydzhou
bd9de4dc4d fix version compare 2024-10-11 11:42:36 +08:00
lloydzhou
2eebfcf6fe client app tauri updater #2966 2024-10-11 11:29:22 +08:00
Lloyd Zhou
c5074f0aa4 Merge pull request #5581 from ConnectAI-E/feature/gemini-functioncall
google gemini support function call
2024-10-10 21:02:36 +08:00
Lloyd Zhou
ba58018a15 Merge pull request #5211 from ConnectAI-E/feature/jest
feat: jest
2024-10-10 21:02:05 +08:00
Lloyd Zhou
63ab83c3c8 Merge pull request #5621 from ConnectAI-E/hotfix/plugin-result
hotfix plugin result is not string #5614
2024-10-10 12:48:55 +08:00
lloydzhou
268cf3b606 hotfix plugin result is not string #5614 2024-10-10 12:47:25 +08:00
Lloyd Zhou
fbc68fa776 Merge pull request #5602 from PeterDaveHello/ImproveTwLocale
i18n: improve tw Traditional Chinese locale
2024-10-09 19:38:06 +08:00
lloydzhou
4ae34ea3ee merge main 2024-10-09 18:27:23 +08:00
Lloyd Zhou
96273fd75e Merge pull request #5611 from ConnectAI-E/feature/tauri-fetch-update
make sure get request_id before body chunk
2024-10-09 16:18:37 +08:00
lloydzhou
3e63d405c1 update 2024-10-09 16:12:01 +08:00
Lloyd Zhou
19b42aac5d Merge pull request #5608 from ConnectAI-E/fix-readme
fix: [#5574] readme
2024-10-09 14:49:34 +08:00
Lloyd Zhou
b67a23200e Merge pull request #5610 from ChatGPTNextWeb/lloydzhou-patch-1
Update README.md
2024-10-09 14:48:55 +08:00
Lloyd Zhou
1dac02e4d6 Update README.md 2024-10-09 14:48:43 +08:00
Lloyd Zhou
acad5b1d08 Merge pull request #5609 from ElricLiu/main
Update README.md
2024-10-09 14:45:27 +08:00
ElricLiu
4e9bb51d2f Update README.md 2024-10-09 14:43:49 +08:00
DDMeaqua
c0c8cdbbf3 fix: [#5574] 文档错误 2024-10-09 14:36:58 +08:00
Lloyd Zhou
cbdc611b54 Merge pull request #5607 from ConnectAI-E/hotfix/summarize-model
fix compressModel, related #5426, fix #5606 #5603 #5575
2024-10-09 14:08:13 +08:00
lloydzhou
93ca303b6c fix ts error 2024-10-09 13:49:33 +08:00
lloydzhou
a925b424a8 fix compressModel, related #5426, fix #5606 #5603 #5575 2024-10-09 13:42:25 +08:00
Lloyd Zhou
5b4d423b58 Merge pull request #5565 from ConnectAI-E/feature/using-tauri-fetch
Feat: using tauri fetch api in App
2024-10-09 13:03:01 +08:00
lloydzhou
6c1cbe120c update 2024-10-09 11:46:49 +08:00
Peter Dave Hello
77a58bc4b0 i18n: improve tw Traditional Chinese locale 2024-10-09 03:14:38 +08:00
Dogtiti
7d55a6d0e4 feat: allow send image only 2024-10-08 21:31:01 +08:00
Dogtiti
8ad63a6c25 Merge pull request #5586 from little-huang/patch-1
fix: correct typo in variable name from ALLOWD_PATH to ALLOWED_PATH
2024-10-08 15:26:41 +08:00
Dogtiti
acf9fa36f9 Merge branch 'main' of https://github.com/ConnectAI-E/ChatGPT-Next-Web into feature/jest 2024-10-08 10:30:47 +08:00
Dogtiti
461154bb03 fix: format package 2024-10-08 10:29:42 +08:00
little_huang
cd75461f9e fix: correct typo in variable name from ALLOWD_PATH to ALLOWED_PATH 2024-10-07 10:30:25 +08:00
Dogtiti
2bac174e6f Merge pull request #4393 from ChatGPTNextWeb/dean-delete-escapeDollarNumber
bugfix: Delete the escapeDollarNumber function, which causes errors i…
2024-10-06 12:41:03 +08:00
Lloyd Zhou
65f80f81ad Merge branch 'main' into dean-delete-escapeDollarNumber 2024-10-04 14:31:00 +08:00
lloydzhou
450766a44b google gemini support function call 2024-10-03 20:28:15 +08:00
Lloyd Zhou
05e6e4bffb Merge pull request #5578 from code-october/fix/safe-equal
use safe equal operation
2024-10-03 10:59:32 +08:00
code-october
fbb66a4a5d use safe equal operation 2024-10-03 02:08:10 +00:00
lloydzhou
d51d31a559 update 2024-10-01 14:40:23 +08:00
lloydzhou
919ee51dca hover show errorMsg when plugin run error 2024-10-01 13:58:50 +08:00
lloydzhou
9c577ad9d5 hotfix for plugin runtime 2024-10-01 12:55:57 +08:00
lloydzhou
953114041b add connect timeout 2024-10-01 12:02:29 +08:00
lloydzhou
d830c23dab hotfix for run plugin call post api 2024-09-30 15:38:13 +08:00
lloydzhou
fd3568c459 hotfix for run plugin call post api 2024-09-30 15:33:40 +08:00
lloydzhou
3029dcb2f6 hotfix for run plugin call post api 2024-09-30 15:32:47 +08:00
lloydzhou
35e03e1bca remove code 2024-09-30 13:44:01 +08:00
Lloyd Zhou
cea5b91f96 Merge pull request #5567 from ChatGPTNextWeb/fix-readme
update  readme
2024-09-30 13:31:34 +08:00
lyf
d2984db6e7 fix readme 2024-09-30 13:28:14 +08:00
lyf
deb215ccd1 fix readme 2024-09-30 13:23:24 +08:00
lloydzhou
7173cf2184 update 2024-09-30 13:07:06 +08:00
Lloyd Zhou
0c697e123d Merge pull request #5564 from code-october/fix/html-code
fix quoteEnd extract regex
2024-09-30 13:06:52 +08:00
lloydzhou
edfa6d14ee update 2024-09-30 10:23:24 +08:00
lloydzhou
b6d9ba93fa update 2024-09-30 10:18:30 +08:00
lloydzhou
6293b95a3b update default api base url 2024-09-30 10:13:11 +08:00
lloydzhou
ef4665cd8b update 2024-09-30 02:57:51 +08:00
lloydzhou
8030e71a5a update 2024-09-30 02:33:02 +08:00
lloydzhou
f42488d4cb using stream fetch replace old tauri http fetch 2024-09-30 02:28:19 +08:00
lloydzhou
af49ed4fdc update 2024-09-30 01:51:14 +08:00
lloydzhou
b174a40634 update 2024-09-30 01:44:27 +08:00
lloydzhou
3c01738c29 update 2024-09-30 01:37:16 +08:00
lloydzhou
9be58f3eb4 fix ts error 2024-09-30 01:30:20 +08:00
lloydzhou
a50c282d01 remove DEFAULT_API_HOST 2024-09-30 01:19:20 +08:00
lloydzhou
5141145e4d revert plugin runtime using tarui/api/http, not using fetch_stream 2024-09-30 00:58:50 +08:00
lloydzhou
b5f6e5a598 update 2024-09-30 00:38:30 +08:00
lloydzhou
7df308d655 Merge remote-tracking branch 'connectai/main' into feature/using-tauri-fetch 2024-09-29 23:36:17 +08:00
code-october
f5ad51a35e fix quoteEnd extract regex 2024-09-29 14:29:42 +00:00
lloydzhou
f9d4105170 stash code 2024-09-29 21:47:38 +08:00
lloydzhou
9e6ee50fa6 using stream_fetch in App 2024-09-29 20:32:36 +08:00
lloydzhou
dd77ad5d74 Merge remote-tracking branch 'connectai/main' into feature/using-tauri-fetch 2024-09-29 19:44:28 +08:00
lloydzhou
3898c507c4 using stream_fetch in App 2024-09-29 19:44:09 +08:00
Lloyd Zhou
fcba50f041 Merge pull request #5547 from ConnectAI-E/hotfix/plugin-opration-id
Hotfix/plugin opration
2024-09-29 16:15:02 +08:00
Lloyd Zhou
452fc86ad1 Merge pull request #5562 from ChatGPTNextWeb/hotfix-google-api
hotfix for `x-goog-api-key`
2024-09-29 15:57:20 +08:00
lloydzhou
5bdf411399 hotfix for x-goog-api-key 2024-09-29 15:51:28 +08:00
lloydzhou
2d920f7ccc using stream: schema to fetch in App 2024-09-28 15:05:41 +08:00
lloydzhou
d84d51b475 using sse: schema to fetch in App 2024-09-28 01:19:39 +08:00
Dogtiti
f9d6f4f9da Merge pull request #5553 from ConnectAI-E/fix/default-model
fix: default model
2024-09-27 21:13:26 +08:00
Lloyd Zhou
a13bd624e8 Merge pull request #5552 from joetsuihk/hotfix/upstream-sync-doc
docs: Hotfix/upstream sync doc update
2024-09-27 20:36:16 +08:00
Joe
8fb019b2e2 revert, leave sync.yml untouched
revert commit 19c4ed4463
2024-09-27 17:34:38 +08:00
Joe
2f3457e73d Update correct links to manualy code update section (JP) 2024-09-27 17:33:02 +08:00
Dogtiti
c6ebd6e73c fix: default model 2024-09-27 17:00:24 +08:00
Joe
2333a47c55 Update links in doc to manual code update section (CN) 2024-09-27 16:50:51 +08:00
Joe
b35895b551 Update correct links to manualy code update section 2024-09-27 16:49:08 +08:00
Joe
19c4ed4463 docs links updated sync.yml
https://github.com/Yidadaa/ChatGPT-Next-Web is renamed to https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/
2024-09-27 16:43:50 +08:00
lloydzhou
22aa1698b4 try using method and path when operationId is undefined #5525 2024-09-27 13:31:49 +08:00
lloydzhou
07d089a2bd try using method and path when operationId is undefined #5525 2024-09-27 13:31:07 +08:00
Dogtiti
870ad913cc Merge pull request #5545 from ConnectAI-E/hotfix/google-auth-header
fix: build error
2024-09-27 11:43:43 +08:00
Dogtiti
3fb389551b fix: build error 2024-09-27 11:42:16 +08:00
Dogtiti
d12a4adfb5 Merge pull request #5541 from ConnectAI-E/hotfix/google-auth-header
google api using `x-google-api-key` header
2024-09-27 11:04:10 +08:00
lloydzhou
702e17c96b google api using x-google-api-key header 2024-09-26 23:21:42 +08:00
Lloyd Zhou
93ff7d26cc Merge pull request #5529 from Leizhenpeng/support-saas-readme
Support saas version in readme
2024-09-25 16:34:25 +08:00
river
13777786c4 chore: ja 2024-09-25 16:30:26 +08:00
river
6655c64e55 chore: cn 2024-09-25 16:29:59 +08:00
Dogtiti
7f3f6f1aaf Merge pull request #5528 from ChatGPTNextWeb/add_tip_top
fix url i18
2024-09-25 16:21:33 +08:00
mayfwl
ea04595c5e Merge branch 'main' into add_tip_top 2024-09-25 16:19:55 +08:00
lyf
13c68bd810 fix url utm 2024-09-25 16:11:57 +08:00
lyf
1d2f44fba8 fix url 2024-09-25 16:00:48 +08:00
lloydzhou
68702bfb1f update version 2024-09-25 15:49:20 +08:00
lyf
e83f61e74d fix 2024-09-25 15:21:17 +08:00
lyf
10d472e79e fix 2024-09-25 14:41:41 +08:00
lyf
a6b920d9af fix 2024-09-25 14:35:09 +08:00
endless-learner
064e964d75 Updated link to deploy on Alibaba Cloud, readable when not logged in, also, able to choose region. 2024-09-24 23:05:32 -07:00
endless-learner
47fb40d572 Merge branch 'ChatGPTNextWeb:main' into main 2024-09-24 23:03:03 -07:00
river
b7892b58f5 chore: support saas 2024-09-25 13:34:04 +08:00
lyf
77f037a3c4 add maidian 2024-09-25 13:08:03 +08:00
lyf
248d27680d fix style 2024-09-25 11:15:00 +08:00
Lloyd Zhou
4c84182e7a Merge pull request #5522 from ConnectAI-E/fix/5494
fix: prevent title update on invalid message response
2024-09-25 10:53:00 +08:00
endless-learner
9e18cc260b Update README.md
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2024-09-24 13:55:00 -07:00
Dogtiti
e8581c8f3c fix: prevent title update on invalid message response 2024-09-25 00:37:37 +08:00
lyf
fe4cba8baf fix style slect button 2024-09-24 22:25:36 +08:00
Dogtiti
9bbd7d3185 Merge pull request #5519 from ConnectAI-E/feature-play-audio-and-video
Feature play audio and video
2024-09-24 22:03:55 +08:00
lloydzhou
dbabb2c403 auto play video/audio 2024-09-24 18:52:54 +08:00
lloydzhou
6c37d04591 auto play video/audio 2024-09-24 18:52:48 +08:00
Dogtiti
649c5be64e Merge pull request #5508 from ConnectAI-E/feature-buildin-plugin
add buildin plugin
2024-09-24 17:28:48 +08:00
Dogtiti
fc0042a799 Merge pull request #5515 from DDMeaqua/config-artifacts
Config artifacts
2024-09-24 17:27:33 +08:00
DDMeaqua
269d064e0a fix: #5450 2024-09-24 15:21:27 +08:00
DDMeaqua
6c8143b7de feat: 全局设置是否启用artifacts 2024-09-24 15:15:08 +08:00
lloydzhou
f9f99639db update 2024-09-24 12:59:21 +08:00
lyf
6d5bf490ab fix media 2024-09-24 11:36:26 +08:00
Dogtiti
46fc2a5012 Merge pull request #5498 from DDMeaqua/fix-plugin-css
Fix plugin css
2024-09-24 10:23:06 +08:00
lloydzhou
90e7b5aecf try using openai api key for dalle-3 plugin 2024-09-23 20:20:20 +08:00
lloydzhou
ed20fd2962 1. add buildin plugin; 2. remove usingProxy 2024-09-23 20:00:07 +08:00
Lloyd Zhou
4c3fd55a75 Merge pull request #5495 from ConnectAI-E/Fix-code-duplication
Fix code duplication
2024-09-23 16:06:59 +08:00
lyf
d95d509046 fex 2024-09-23 15:43:36 +08:00
lyf
c15c852668 fex media 2024-09-23 15:12:45 +08:00
DDMeaqua
4a60512ae7 chore: css 2024-09-23 14:18:32 +08:00
DDMeaqua
0e210cf8de fix: #5486 plugin样式优化 2024-09-23 14:13:09 +08:00
lyf
35aa2c7270 Fix code duplication 2024-09-23 11:34:20 +08:00
lyf
518e0d90a5 fex url 2024-09-23 11:11:36 +08:00
lyf
51f7b02b27 fex en 2024-09-23 10:56:43 +08:00
Lloyd Zhou
23f2b6213c Merge pull request #5489 from ConnectAI-E/feature-fix-openai-function-call
Feature fix openai function call
2024-09-22 19:08:35 +08:00
lloydzhou
3a969054e3 hotfix openai function call tool_calls no index 2024-09-22 18:59:49 +08:00
lloydzhou
4d1f9e49d4 hotfix openai function call tool_calls no index 2024-09-22 18:53:51 +08:00
lyf
702f5bd362 fex setCookie 2024-09-20 10:22:22 +08:00
Dogtiti
2474d5b6d2 Merge pull request #5304 from dustookk/main
fix no max_tokens in payload when the vision model name does not cont…
2024-09-19 20:34:23 +08:00
lyf
9858d1f958 add top tip 2024-09-19 18:21:26 +08:00
lyf
62efab987b add fanyi add top tip 2024-09-19 16:58:51 +08:00
Dogtiti
c75d9e3de4 Merge pull request #5463 from yudshj/main
修正了typo,WHITE_WEBDEV_ENDPOINTS -> WHITE_WEBDAV_ENDPOINTS
2024-09-19 14:26:48 +08:00
Yudong
df222ded12 修正了typo, WebDev -> WebDav 2024-09-19 14:15:31 +08:00
river
7dc0f81d3f chore: change placeholder 2024-09-19 13:48:59 +08:00
river
23793e834d chore: change placeholder 2024-09-19 12:53:43 +08:00
river
f4f3c6ad5a chore: change placeholder 2024-09-19 12:47:09 +08:00
river
775794e0e4 chore: add setting 2024-09-19 12:38:46 +08:00
endless-learner
03268ce4d8 Added 1-click deployment link for Alibaba Cloud. 2024-09-18 20:38:20 -07:00
Yudong
212d15fdd0 修正了typo,WHITE_WEBDEV_ENDPOINTS -> WHITE_WEBDAV_ENDPOINTS 2024-09-19 11:20:18 +08:00
river
065f015f7b feat: add error tip 2024-09-19 09:53:00 +08:00
Dogtiti
b5ba05dd83 Merge pull request #5462 from JuliusMoehring/main
fix: Avoid fetching prompts.json serverside
2024-09-19 09:50:21 +08:00
river
e4fda6cacf feat: add auth tip 2024-09-19 08:41:09 +08:00
JuliusMoehring
accb526cd6 Avoid fetching prompts.json serverside 2024-09-18 18:07:10 +02:00
river
2f0d94a46b chore: add auth tip 2024-09-19 00:05:06 +08:00
river
8dc24403d8 chore: Update Chinese translation for API key placeholder 2024-09-18 22:05:51 +08:00
Dogtiti
a8c70d84a9 Merge pull request #5459 from DDMeaqua/tts
add tts
2024-09-18 15:42:16 +08:00
DDMeaqua
10d7a64f88 fix: error 2024-09-18 15:37:21 +08:00
Dogtiti
d51bbb4a81 Merge pull request #5444 from skymkmk/pr-fix-model-config-hydration
fix: config hydration and default model forced to set gpt-3.5-turbo
2024-09-18 15:26:47 +08:00
Dogtiti
848f794149 Merge pull request #5402 from DDMeaqua/fix-selector-css
fix: selector css
2024-09-18 15:19:08 +08:00
DDMeaqua
7f1b44befe fix: css 2024-09-18 15:04:41 +08:00
Dogtiti
9ddd5a0566 Merge pull request #5432 from ConnectAI-E/Feature-fork
feat fork
2024-09-18 15:04:27 +08:00
DDMeaqua
a3b664763e chore: default header 2024-09-18 14:57:43 +08:00
lyf
fd47bc1dc3 Add English copy 2024-09-18 13:56:44 +08:00
DDMeaqua
dfaafe3adb Merge branch 'main' into tts 2024-09-18 13:48:28 +08:00
Dogtiti
b4dc4d34eb Merge pull request #5112 from DDDDD12138/remove-unused-imports
Chore: Remove Unused Imports and Add ESLint Rules
2024-09-18 11:37:47 +08:00
DDMeaqua
3ae8ec1af6 feat: tts 2024-09-18 11:24:25 +08:00
Lloyd Zhou
5c34666334 Merge pull request #5454 from SukkaW/ci-bump
ci: bump `actions/cache` to v4
2024-09-18 11:13:59 +08:00
DDMeaqua
212605a7e3 Merge branch 'main' into tts-stt 2024-09-18 10:39:56 +08:00
SukkaW
4ddfa9af8d ci: bump actions/cache to v4 2024-09-17 22:28:13 +08:00
skymkmk
36a0c7b8a3 fix: default is forced to set gpt-3.5-turbo if no server default model have been set 2024-09-16 02:07:22 +08:00
skymkmk
9e1e0a7252 fix: persisted available models ard not be update after source code have been updated 2024-09-16 02:06:17 +08:00
Dogtiti
027e5adf67 Merge pull request #5442 from DDDDD12138/fix-typo
chore: correct typo
2024-09-15 22:12:59 +08:00
DDDDD12138
e986088bec chore: correct typo 2024-09-15 21:59:21 +08:00
DDDDD12138
63ffd473d5 chore: remove unused imports 2024-09-15 20:17:02 +08:00
Dogtiti
9e5d92dc58 Merge pull request #5438 from DDMeaqua/fix-AnthropicCros
fix: ts error
2024-09-15 14:23:29 +08:00
Meaqua
8ac9141a29 fix: ts error 2024-09-15 14:21:27 +08:00
Dogtiti
313c942350 Merge pull request #5435 from DDMeaqua/fix-AnthropicCros
fix: #5429 Anthropic authentication_error CORS
2024-09-15 10:07:24 +08:00
Dogtiti
26c3edd023 Merge pull request #5430 from skymkmk/pr-manual-regen-title
feat: menual regen title
2024-09-14 18:10:32 +08:00
DDMeaqua
9a5a3d4ce4 fix: #5429 Anthropic authentication_error CORS 2024-09-14 16:06:18 +08:00
lyf
6e79b9a7a2 add fork 2024-09-14 14:19:11 +08:00
Dogtiti
b32d82e6c1 Merge pull request #5426 from skymkmk/pr-summarize-customization
feat: summarize model customization
2024-09-14 14:18:25 +08:00
wuzhiqing
a3585685df chore: add ESLint plugin and rules to remove unused imports
- Installed eslint-plugin-unused-imports
- Updated .eslintrc.json to include rules for detecting unused imports
2024-09-14 13:54:51 +08:00
Dogtiti
f379865e2c Merge pull request #5431 from tuanzisama/tuanzisama-patch-1
feat: Improve setting.model selector
2024-09-14 10:05:38 +08:00
Dogtiti
4eb4c31438 Merge pull request #5428 from Dakai/main
Add a space between model and provider in ModelSelector to improve readability.
2024-09-14 09:57:52 +08:00
evenwan
84a7afcd94 feat: Improve setting.model selector 2024-09-14 09:31:05 +08:00
skymkmk
fa48ace39b fix: prevent users from setting a extremly short history that resulting in no content being sent for the title summary 2024-09-14 07:49:26 +08:00
skymkmk
1b869d9305 translation: translations by claude for new writings 2024-09-14 07:42:06 +08:00
skymkmk
93bc2f5870 feat: now user can choose their own summarize model 2024-09-14 07:41:27 +08:00
skymkmk
37c0cfe1e9 translation: translations by claude for manual refresh 2024-09-14 07:28:07 +08:00
skymkmk
fc27441561 feat: manual refresh for title 2024-09-14 07:21:19 +08:00
MatrixDynamo
79cfbac11f Add a space between model and provider in ModelSelector to improve readability. 2024-09-14 03:19:24 +08:00
lloydzhou
df62736ff6 update version 2024-09-13 17:36:32 +08:00
Dogtiti
6a464b3e5f Merge pull request #5418 from ConnectAI-E/hotfix/artifact
fixed: html codeblock include 2 newline
2024-09-13 17:32:21 +08:00
Lloyd Zhou
57fcda80df Merge pull request #5419 from DDMeaqua/feat-shortcutkey
chore: 手机端隐藏快捷键展示
2024-09-13 17:31:22 +08:00
DDMeaqua
db39fbc419 chore: 手机端隐藏快捷键展示 2024-09-13 16:56:06 +08:00
lloydzhou
3dabe47c78 fixed: html codeblock include 2 newline 2024-09-13 16:27:02 +08:00
Dogtiti
affc194cde Merge pull request #5416 from skymkmk/pr-add-o1
feat: add o1 model
2024-09-13 16:26:42 +08:00
skymkmk
03fa580a55 fix: give o1 some time to think twice 2024-09-13 16:25:04 +08:00
skymkmk
d0dce654bf fix: shouldstream is not depend on iso1 2024-09-13 14:18:18 +08:00
Dogtiti
169323e238 Merge pull request #5415 from skymkmk/pr-fix-incorrect-vision-model-judgement
fix: remove the visual model judgment method that checks if the model…
2024-09-13 13:57:37 +08:00
skymkmk
71df415b14 feat: add o1 model 2024-09-13 13:34:49 +08:00
skymkmk
6bb01bc564 fix: remove the visual model judgment method that checks if the model name contains 'preview' from the openai api to prevent models like o1-preview from being classified as visual models 2024-09-13 12:56:28 +08:00
Dogtiti
07c6fe5975 Merge pull request #5406 from ChatGPTNextWeb/river
fix: #4240 remove tip when 0 context
2024-09-12 21:48:11 +08:00
Dogtiti
5964181cb2 Merge pull request #5405 from xmcp/patch-1
fix typo in UI
2024-09-12 21:47:39 +08:00
river
4b8288a2b2 fix: #4240 remove tip when 0 context 2024-09-12 21:05:02 +08:00
xmcp
88b1c1c6a5 fix typo 2024-09-12 19:54:16 +08:00
Dogtiti
1234deabfa Merge pull request #5396 from DDMeaqua/feat-shortcutkey
feat: add shortcut key
2024-09-12 13:57:24 +08:00
DDMeaqua
ebaeb5a0d5 fix: selector css 2024-09-11 17:54:48 +08:00
DDMeaqua
45306bbb6c fix: ts error 2024-09-10 14:37:58 +08:00
DDMeaqua
18e2403b01 chore: 更换icon 2024-09-10 14:30:51 +08:00
DDMeaqua
e578c5f3ad chore: 样式更新 2024-09-10 12:01:51 +08:00
DDMeaqua
61245e3d7e fix: dark theme css 2024-09-09 19:29:10 +08:00
DDMeaqua
7804182d0d fix: type error 2024-09-09 19:18:12 +08:00
DDMeaqua
f2195154f6 feat: add shortcut key 2024-09-09 18:55:37 +08:00
Dogtiti
35f77f45a2 Merge pull request #5386 from ConnectAI-E/feature/safeLocalStorage
fix: safaLocalStorage
2024-09-09 16:48:25 +08:00
Dogtiti
992c3a5d3a fix: safaLocalStorage 2024-09-08 13:23:40 +08:00
mayfwl
d51d7b6797 Merge pull request #5376 from MrrDrr/add_chatgpt_4o_latest
add chatgpt-4o-latest
2024-09-08 10:15:41 +08:00
lloydzhou
23ac2efd89 hotfix and update version 2024-09-07 22:12:42 +08:00
Lloyd Zhou
daeffb2dc6 Merge pull request #5383 from SukkaW/fix-5378
fix(#5378): default plugin ids to empty array
2024-09-07 22:09:35 +08:00
SukkaW
db58ca6c1d fix(#5378): default plugin ids to empty array 2024-09-07 21:32:18 +08:00
Lloyd Zhou
2ff292cbfa Merge pull request #5381 from reggiezhang/patch-1
Add crossOrigin="use-credentials" for site.webmanifest
2024-09-07 16:58:07 +08:00
Reggie Zhang
5a81393863 Add crossOrigin="use-credentials" for site.webmanifest
Add `crossOrigin="use-credentials"` to the `<link>` element for `site.webmanifest` when the site is behind a proxy with authentication.
2024-09-07 16:24:52 +08:00
Lloyd Zhou
116a73d398 Merge pull request #5377 from ConnectAI-E/hotfix/mermaid
hotfix Mermaid can not render. close #5374
2024-09-07 13:01:36 +08:00
lloydzhou
cf0c057164 hotfix Mermaid can not render. close #5374 2024-09-07 13:00:55 +08:00
Lloyd Zhou
fe5a4f4447 Merge pull request #5375 from Kosette/fix-version
fix: update package version
2024-09-07 11:55:23 +08:00
l.tingting
c1b74201e4 add chatgpt-4o-latest 2024-09-07 01:42:56 +08:00
kosette
27828d9ca8 fix: update package version 2024-09-06 23:07:01 +08:00
Dogtiti
2bd799fac6 Merge pull request #5331 from ConnectAI-E/feature/plugin
Feature plugin (GPTs like action based on function call)
2024-09-06 20:06:50 +08:00
lloydzhou
9275f2d753 add awesome plugin repo url 2024-09-06 19:37:24 +08:00
lloydzhou
7455978ee5 default enable artifact 2024-09-06 09:26:06 +08:00
lloydzhou
7c0acc7b77 hotfix tools empty array 2024-09-05 22:02:06 +08:00
Lloyd Zhou
f32dd69acf Merge pull request #7 from ConnectAI-E/feature/plugin-artifact
Feature/plugin artifact
2024-09-05 14:54:01 +08:00
lloydzhou
80b8f956a9 move artifacts into mask settings 2024-09-05 14:49:11 +08:00
lloydzhou
caf50b6e6c move artifacts into mask settings 2024-09-05 14:46:16 +08:00
lloydzhou
b590d0857c disable nextjs proxy, then can using dalle as plugin 2024-09-05 13:51:59 +08:00
Lloyd Zhou
982019307c Merge pull request #6 from ConnectAI-E/feature/plugin-app-cors
using tauri http api run plugin to fixed cors in App
2024-09-04 22:49:35 +08:00
lloydzhou
09aec7b22e using tauri http api run plugin to fixed cors in App 2024-09-04 21:32:22 +08:00
lloydzhou
f9a047aad4 using tauri http api run plugin to fixed cors in App 2024-09-04 21:04:13 +08:00
Lloyd Zhou
85704570f3 Merge pull request #5356 from ConnectAI-E/feature/indexdb
fix: hydrated for indexedDB
2024-09-04 19:14:18 +08:00
lloydzhou
53dcae9e9c update 2024-09-04 13:00:18 +08:00
lloydzhou
04e1ab63bb update readme 2024-09-04 11:47:42 +08:00
Dogtiti
ed9aae531e fix: hydrated 2024-09-03 20:29:01 +08:00
lloydzhou
6ab6b3dbca remove no need code 2024-09-03 20:21:37 +08:00
lloydzhou
7180ed9a60 hotfix 2024-09-03 19:56:22 +08:00
lloydzhou
0a5522d28c update 2024-09-03 19:35:36 +08:00
lloydzhou
c7bc93b32b Merge remote-tracking branch 'connectai/main' into feature/plugin 2024-09-03 19:02:40 +08:00
lloydzhou
d30351e7b0 update readme 2024-09-03 17:18:43 +08:00
Dogtiti
886ffc0af8 fix: hydrated for indexedDB 2024-09-03 17:12:48 +08:00
lloydzhou
4fdd997108 hotfix 2024-09-03 16:23:54 +08:00
lloydzhou
236736deea remove no need code 2024-09-03 15:37:23 +08:00
lloydzhou
2b317f60c8 add config auth location 2024-09-03 12:00:55 +08:00
lloydzhou
3ec67f9f47 add load from url 2024-09-03 00:45:11 +08:00
lloydzhou
6435e7a30e update readme 2024-09-02 23:42:56 +08:00
lloydzhou
078305f5ac kimi support function call 2024-09-02 21:55:17 +08:00
lloydzhou
801b62543a claude support function call 2024-09-02 21:45:47 +08:00
lloydzhou
877668b629 hotfix 2024-09-02 18:29:00 +08:00
lloydzhou
f652f73260 plugin add auth config 2024-09-02 18:11:19 +08:00
lloydzhou
b2965e1deb update 2024-08-31 00:16:47 +08:00
lloydzhou
2214689920 add gapier proxy 2024-08-30 23:51:03 +08:00
lloydzhou
9326ff9d08 ts error 2024-08-30 23:39:08 +08:00
lloydzhou
271f58d9cf stash code 2024-08-30 17:31:20 +08:00
lloydzhou
cac99e3908 add Plugin page 2024-08-30 13:02:03 +08:00
Dogtiti
97a4a910e0 Merge pull request #5335 from ConnectAI-E/fix/right-click
fix: right click
2024-08-29 20:49:31 +08:00
Dogtiti
19c7a84548 fix: right click 2024-08-29 20:48:04 +08:00
lloydzhou
571ce11e53 stash code 2024-08-29 19:55:09 +08:00
lloydzhou
d2cb984ced add processToolMessage callback 2024-08-29 17:28:15 +08:00
lloydzhou
7fc0d11931 create common function stream for fetchEventSource 2024-08-29 17:14:23 +08:00
lloydzhou
341a52a615 stash code 2024-08-29 01:35:41 +08:00
lloydzhou
d58b99d602 stash code 2024-08-29 01:00:16 +08:00
lloydzhou
f7a5f836db stash code 2024-08-29 00:56:20 +08:00
lloydzhou
d212df8b95 stash code 2024-08-29 00:39:51 +08:00
lloydzhou
f3f6dc57c3 stash code 2024-08-29 00:32:35 +08:00
lloydzhou
29b5cd9436 ts error 2024-08-29 00:21:26 +08:00
lloydzhou
f5209fc344 stash code 2024-08-28 23:58:46 +08:00
DDMeaqua
c5168c2132 fix: i18n 2024-08-28 13:15:52 +08:00
DDMeaqua
318e0989a2 fix: transcription headers 2024-08-28 13:13:41 +08:00
DDMeaqua
d8b1781d7b Merge branch 'tts-stt' of https://github.com/DDMeaqua/ChatGPT-Next-Web into tts-stt 2024-08-28 12:40:20 +08:00
DDMeaqua
ed5aea0521 fix: bug 2024-08-28 12:37:19 +08:00
Meaqua
e9f90a4d82 fix: i18n 2024-08-27 21:49:00 +08:00
DDMeaqua
f86b220c92 feat: add voice action 2024-08-27 19:50:16 +08:00
Lloyd Zhou
b6bb1673d4 Merge pull request #5324 from ConnectAI-E/feature/indexdb
feat: add indexDB
2024-08-27 18:07:20 +08:00
DDMeaqua
93f1762e6c chore: wip 2024-08-27 17:02:44 +08:00
DDMeaqua
2f410fc09f feat: add tts stt 2024-08-27 16:21:02 +08:00
Dogtiti
7b6fe66f2a feat: try catch indexedDB error 2024-08-27 10:05:37 +08:00
Dogtiti
c2fc0b4979 feat: try catch indexedDB error 2024-08-27 09:57:07 +08:00
Dogtiti
0b758941a4 feat: clear indexDB 2024-08-26 21:23:21 +08:00
Dogtiti
492b55c893 feat: add indexDB 2024-08-26 21:20:07 +08:00
Dogtiti
4060e367ad feat: add indexDB 2024-08-26 21:13:35 +08:00
lloydzhou
c99cd31b6b add openapi-client-axios 2024-08-24 15:34:33 +08:00
Lloyd Zhou
718782f5b1 Merge pull request #5309 from ElricLiu/main
Update README.md
2024-08-24 12:11:46 +08:00
ElricLiu
0c3fb5b2ce Update README.md
add monica sponsored
2024-08-23 17:28:00 +08:00
mayfwl
4ec6b067e7 fix: artifact render error (#5306)
fix: artifact render error
2024-08-21 18:48:44 +08:00
Dogtiti
1748dd6a3b Merge pull request #5303 from ConnectAI-E/Modifylang
Modify View All Languages
2024-08-21 16:28:36 +08:00
mayfwl
95332e50ed Merge branch 'main' into Modifylang 2024-08-21 15:45:04 +08:00
yihang3
56eb9d1430 fix no max_tokens in payload when the vision model name does not contain 'vision'. 2024-08-21 15:22:31 +08:00
lyf
8496980cf9 Modify View All Languages 2024-08-21 14:28:39 +08:00
Lloyd Zhou
ffe32694b0 Merge pull request #5300 from ConnectAI-E/hotfix/hide-button
Hotfix/hide button
2024-08-21 14:12:03 +08:00
lloydzhou
3d5b21154b update 2024-08-21 12:05:03 +08:00
lloydzhou
4b9697e336 fix: typescript error 2024-08-21 11:54:48 +08:00
lloydzhou
b0e9a542ba frat: add reload button 2024-08-21 11:17:00 +08:00
lloydzhou
8b67536c23 fix: 修复多余的查看全部 2024-08-21 10:28:34 +08:00
lloydzhou
cd49c12181 fix: 修复查看全部按钮导致artifacts失效 2024-08-21 10:27:37 +08:00
Lloyd Zhou
a6b14c7910 Merge pull request #5274 from Movelocity/feat/search-history
feat: add a page to search chat history
2024-08-21 00:58:01 +08:00
heweikang
e275abdb9c match the origin format 2024-08-20 19:49:47 +08:00
heweikang
09a90665d5 Merge branch 'feat/search-history' of https://github.com/Movelocity/ChatGPT-Next-Web into feat/search-history 2024-08-20 19:47:41 +08:00
heweikang
6649fbdfd0 remove an empty line 2024-08-20 19:47:36 +08:00
heweikang
64a0ffee7b Merge branch 'main' into feat/search-history 2024-08-20 19:47:03 +08:00
Hollway
b529118f31 Merge branch 'ChatGPTNextWeb:main' into feat/search-history 2024-08-20 19:46:08 +08:00
heweikang
39d7d9f13a migrate the search button to plugins discovery 2024-08-20 19:44:22 +08:00
heweikang
fcd55df969 wrap doSearch with useCallback 2024-08-20 09:45:34 +08:00
Dogtiti
1e59948358 Merge pull request #5288 from zhangjian10/main
fix: Determine if Tencent is authorized
2024-08-19 09:20:58 +08:00
zhangjian10
1102ef6e6b fix: Determine if Tencent is authorized 2024-08-18 23:47:23 +08:00
heweikang
7ce2e8f4c4 resolve a warning 2024-08-17 11:26:38 +08:00
heweikang
fd1c656bdd add all translations for SearchChat 2024-08-17 11:08:38 +08:00
heweikang
82298a760a Merge branch 'main' into feat/search-history 2024-08-17 10:15:49 +08:00
heweikang
b84bb72e07 add null check for search content 2024-08-17 10:06:56 +08:00
lloydzhou
495b321d0a Merge remote-tracking branch 'origin/main' 2024-08-16 16:35:31 +08:00
lloydzhou
8a38cdc1d7 update version 2024-08-16 16:35:13 +08:00
Lloyd Zhou
e210db7bd9 Merge pull request #5235 from ConnectAI-E/feature/access
代码折叠
2024-08-16 16:34:13 +08:00
Lloyd Zhou
f64763b74b Merge pull request #5093 from DDMeaqua/feat-i18n
feat: update i18n
2024-08-16 16:20:31 +08:00
Lloyd Zhou
e033bef544 Merge branch 'main' into feat-i18n 2024-08-16 16:13:13 +08:00
DDMeaqua
d88cc575e5 chore: update 2024-08-16 16:10:31 +08:00
heweikang
e3f499be0c hide search button text 2024-08-16 10:23:27 +08:00
Dogtiti
87325fad74 Merge pull request #5270 from DDMeaqua/main
feat: add model name to conversation page
2024-08-15 20:12:33 +08:00
Hollway
86220573b6 Update yarn.lock to match origin version 2024-08-15 17:27:14 +08:00
heweikang
65ed6b02a4 Merge branch 'main' into feat/search-history 2024-08-15 17:24:17 +08:00
heweikang
98093a1f31 make search result item easier to click 2024-08-15 17:21:39 +08:00
heweikang
00990dc195 use yarn instead of npm 2024-08-15 17:09:41 +08:00
Lloyd Zhou
122aa94c9f Merge pull request #5273 from ElricLiu/main
Update README.md
2024-08-15 14:35:48 +08:00
heweikang
3da5284a07 优化搜索算法,更新图标 2024-08-15 12:38:20 +08:00
heweikang
cd920364f8 Add page to search chat history 2024-08-14 22:28:05 +08:00
ElricLiu
e2e8a45104 Update README.md 2024-08-14 20:38:52 +08:00
DDMeaqua
fb5fc13f72 feat: add model name 2024-08-13 19:58:44 +08:00
Dogtiti
edb92f7bfb Merge pull request #5245 from MrrDrr/dalle3_add_options
dall-e-3 adds 'quality' and 'style' options
2024-08-13 15:31:18 +08:00
lyf
1980f43b9f feat(auth): xg feature 2024-08-12 16:31:12 +08:00
lyf
c3c3dd5154 feature add zhedie 2024-08-12 14:06:21 +08:00
lyf
356155fd30 feature add zhedie 2024-08-12 13:58:53 +08:00
lyf
6f75ef8f0a addcd 2024-08-10 12:45:50 +08:00
l.tingting
ca865a80dc dall-e-3 adds 'quality' and 'style' options 2024-08-10 11:09:07 +08:00
lyf
8f759d1c3e 代码折叠 2024-08-09 14:55:25 +08:00
lyf
44787637f2 tj 2024-08-08 21:07:03 +08:00
Dogtiti
cf1c8e8f2a Merge pull request #5233 from ConnectAI-E/feature/bangpt
Feature/bangpt
2024-08-08 17:27:50 +08:00
lyf
d948be2372 Merge branch 'feature/bangpt' of ssh://github.com/ConnectAI-E/ChatGPT-Next-Web into feature/bangpt 2024-08-08 16:25:49 +08:00
lyf
cc28aef625 add gpt-4o-2024-08-06 2024-08-08 16:23:40 +08:00
Dogtiti
036358de7c Merge pull request #5219 from ConnectAI-E/feature/access
修复ChatGPTNextWeb页面的无障碍问题
2024-08-08 13:59:16 +08:00
lyf
0958b9ee12 tsxiugai 2024-08-08 13:08:56 +08:00
Dogtiti
aff1d7ecd6 Merge pull request #5223 from 122cygf/main
修复拼写错误
2024-08-07 21:29:43 +08:00
lzz
42fdbd9bb8 Fix spelling errors 2024-08-07 17:22:32 +08:00
lyf
034c82e514 修改设置的无障碍 2024-08-07 13:39:23 +08:00
lyf
14ff46b5cd 解决按钮无障碍 2024-08-07 13:01:08 +08:00
lyf
c9099ca0a5 无障碍按钮和链接 2024-08-07 10:55:02 +08:00
Dogtiti
58b144b345 Merge pull request #5218 from ConnectAI-E/feature/bangpt
解决禁止gpt4时禁止gtp4o-mini问题
2024-08-07 10:45:38 +08:00
lyf
af21c57e77 ban gpt4 2024-08-07 10:15:39 +08:00
Dogtiti
1287e39cc6 feat: run test before build 2024-08-06 19:24:47 +08:00
Dogtiti
1ef2aa35e9 feat: jest 2024-08-06 18:03:27 +08:00
DDMeaqua
9c7bacc65e chore: update 2024-07-24 10:45:17 +08:00
DDMeaqua
86b8bfcb1f feat: update i18n 2024-07-23 15:21:38 +08:00
butterfly
4d6b981a54 bugfix: Delete the escapeDollarNumber function, which causes errors in rendering a latex string 2024-03-26 11:43:55 +08:00
160 changed files with 16709 additions and 3405 deletions

View File

@@ -66,4 +66,4 @@ ANTHROPIC_API_VERSION=
ANTHROPIC_URL=
### (optional)
WHITE_WEBDEV_ENDPOINTS=
WHITE_WEBDAV_ENDPOINTS=

View File

@@ -1,4 +1,7 @@
{
"extends": "next/core-web-vitals",
"plugins": ["prettier"]
"plugins": ["prettier", "unused-imports"],
"rules": {
"unused-imports/no-unused-imports": "warn"
}
}

View File

@@ -3,9 +3,7 @@ name: VercelPreviewDeployment
on:
pull_request_target:
types:
- opened
- synchronize
- reopened
- review_requested
env:
VERCEL_TEAM: ${{ secrets.VERCEL_TEAM }}
@@ -49,7 +47,7 @@ jobs:
run: npm install --global vercel@latest
- name: Cache dependencies
uses: actions/cache@v2
uses: actions/cache@v4
id: cache-npm
with:
path: ~/.npm

39
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,39 @@
name: Run Tests
on:
push:
branches:
- main
tags:
- "!*"
pull_request:
types:
- review_requested
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: 18
cache: "yarn"
- name: Cache node_modules
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-node_modules-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-node_modules-
- name: Install dependencies
run: yarn install
- name: Run Jest tests
run: yarn test:ci

View File

@@ -1,26 +1,30 @@
<div align="center">
<a href='#企业版'>
<img src="./docs/images/ent.svg" alt="icon"/>
<a href='https://nextchat.dev/chat'>
<img src="https://github.com/user-attachments/assets/287c510f-f508-478e-ade3-54d30453dc18" width="1000" alt="icon"/>
</a>
<h1 align="center">NextChat (ChatGPT Next Web)</h1>
English / [简体中文](./README_CN.md)
One-Click to get a well-designed cross-platform ChatGPT web UI, with GPT3, GPT4 & Gemini Pro support.
One-Click to get a well-designed cross-platform ChatGPT web UI, with Claude, GPT4 & Gemini Pro support.
一键免费部署你的跨平台私人 ChatGPT 应用, 支持 GPT3, GPT4 & Gemini Pro 模型。
一键免费部署你的跨平台私人 ChatGPT 应用, 支持 Claude, GPT4 & Gemini Pro 模型。
[![Saas][Saas-image]][saas-url]
[![Web][Web-image]][web-url]
[![Windows][Windows-image]][download-url]
[![MacOS][MacOS-image]][download-url]
[![Linux][Linux-image]][download-url]
[Web App](https://app.nextchat.dev/) / [Desktop App](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [Discord](https://discord.gg/YCkeafCafC) / [Enterprise Edition](#enterprise-edition) / [Twitter](https://twitter.com/NextChatDev)
[NextChatAI](https://nextchat.dev/chat?utm_source=readme) / [Web App](https://app.nextchat.dev) / [Desktop App](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [Discord](https://discord.gg/YCkeafCafC) / [Enterprise Edition](#enterprise-edition) / [Twitter](https://twitter.com/NextChatDev)
[网页版](https://app.nextchat.dev/) / [客户端](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [企业版](#%E4%BC%81%E4%B8%9A%E7%89%88) / [反馈](https://github.com/Yidadaa/ChatGPT-Next-Web/issues)
[NextChatAI](https://nextchat.dev/chat) / [网页版](https://app.nextchat.dev) / [客户端](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [企业版](#%E4%BC%81%E4%B8%9A%E7%89%88) / [反馈](https://github.com/Yidadaa/ChatGPT-Next-Web/issues)
[saas-url]: https://nextchat.dev/chat?utm_source=readme
[saas-image]: https://img.shields.io/badge/NextChat-Saas-green?logo=microsoftedge
[web-url]: https://app.nextchat.dev/
[download-url]: https://github.com/Yidadaa/ChatGPT-Next-Web/releases
[Web-image]: https://img.shields.io/badge/Web-PWA-orange?logo=microsoftedge
@@ -28,7 +32,9 @@ One-Click to get a well-designed cross-platform ChatGPT web UI, with GPT3, GPT4
[MacOS-image]: https://img.shields.io/badge/-MacOS-black?logo=apple
[Linux-image]: https://img.shields.io/badge/-Linux-333?logo=ubuntu
[<img src="https://vercel.com/button" alt="Deploy on Zeabur" height="30">](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FChatGPTNextWeb%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=nextchat&repository-name=NextChat) [<img src="https://zeabur.com/button.svg" alt="Deploy on Zeabur" height="30">](https://zeabur.com/templates/ZBUEFA) [<img src="https://gitpod.io/button/open-in-gitpod.svg" alt="Open in Gitpod" height="30">](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
[<img src="https://vercel.com/button" alt="Deploy on Vercel" height="30">](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FChatGPTNextWeb%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=nextchat&repository-name=NextChat) [<img src="https://zeabur.com/button.svg" alt="Deploy on Zeabur" height="30">](https://zeabur.com/templates/ZBUEFA) [<img src="https://gitpod.io/button/open-in-gitpod.svg" alt="Open in Gitpod" height="30">](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web) [<img src="https://img.shields.io/badge/BT_Deploy-Install-20a53a" alt="BT Deply Install" height="30">](https://www.bt.cn/new/download.html)
[<img src="https://github.com/user-attachments/assets/903482d4-3e87-4134-9af1-f2588fa90659" height="60" width="288" >](https://monica.im/?utm=nxcrp)
</div>
@@ -58,7 +64,7 @@ For enterprise inquiries, please contact: **business@nextchat.dev**
企业版咨询: **business@nextchat.dev**
<img width="300" src="https://github.com/user-attachments/assets/3daeb7b6-ab63-4542-9141-2e4a12c80601">
<img width="300" src="https://github.com/user-attachments/assets/3d4305ac-6e95-489e-884b-51d51db5f692">
## Features
@@ -89,12 +95,15 @@ For enterprise inquiries, please contact: **business@nextchat.dev**
- [x] Desktop App with tauri
- [x] Self-host Model: Fully compatible with [RWKV-Runner](https://github.com/josStorer/RWKV-Runner), as well as server deployment of [LocalAI](https://github.com/go-skynet/LocalAI): llama/gpt4all/rwkv/vicuna/koala/gpt4all-j/cerebras/falcon/dolly etc.
- [x] 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, network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
- [x] Plugins: support network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353)
- [x] network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353)
- [x] Supports Realtime Chat [#5672](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5672)
- [ ] local knowledge base
## What's New
- 🚀 v2.15.8 Now supports Realtime Chat [#5672](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5672)
- 🚀 v2.15.4 The Application supports using Tauri fetch LLM API, MORE SECURITY! [#5379](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5379)
- 🚀 v2.15.0 Now supports Plugins! Read this: [NextChat-Awesome-Plugins](https://github.com/ChatGPTNextWeb/NextChat-Awesome-Plugins)
- 🚀 v2.14.0 Now supports Artifacts & SD
- 🚀 v2.10.1 support Google Gemini Pro model.
- 🚀 v2.9.11 you can use azure endpoint now.
@@ -125,12 +134,15 @@ For enterprise inquiries, please contact: **business@nextchat.dev**
- [x] 使用 tauri 打包桌面应用
- [x] 支持自部署的大语言模型:开箱即用 [RWKV-Runner](https://github.com/josStorer/RWKV-Runner) ,服务端部署 [LocalAI 项目](https://github.com/go-skynet/LocalAI) llama / gpt4all / rwkv / vicuna / koala / gpt4all-j / cerebras / falcon / dolly 等等,或者使用 [api-for-open-llm](https://github.com/xusenlinzy/api-for-open-llm)
- [x] Artifacts: 通过独立窗口,轻松预览、复制和分享生成的内容/可交互网页 [#5092](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/pull/5092)
- [x] 插件机制,支持 artifacts联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
- [x] artifacts
- [ ] 支持联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
- [x] 插件机制,支持`联网搜索``计算器`、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353)
- [x] 支持联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353)
- [x] 支持 Realtime Chat [#5672](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5672)
- [ ] 本地知识库
## 最新动态
- 🚀 v2.15.8 现在支持Realtime Chat [#5672](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5672)
- 🚀 v2.15.4 客户端支持Tauri本地直接调用大模型API更安全[#5379](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5379)
- 🚀 v2.15.0 现在支持插件功能了!了解更多:[NextChat-Awesome-Plugins](https://github.com/ChatGPTNextWeb/NextChat-Awesome-Plugins)
- 🚀 v2.14.0 现在支持 Artifacts & SD 了。
- 🚀 v2.10.1 现在支持 Gemini Pro 模型。
- 🚀 v2.9.11 现在可以使用自定义 Azure 服务了。
@@ -168,7 +180,7 @@ We recommend that you follow the steps below to re-deploy:
### Enable Automatic Updates
> If you encounter a failure of Upstream Sync execution, please manually sync fork once.
> If you encounter a failure of Upstream Sync execution, please [manually update code](./README.md#manually-updating-code).
After forking the project, due to the limitations imposed by GitHub, you need to manually enable Workflows and Upstream Sync Action on the Actions page of the forked project. Once enabled, automatic updates will be scheduled every hour:
@@ -292,6 +304,14 @@ iflytek Api Key.
iflytek Api Secret.
### `CHATGLM_API_KEY` (optional)
ChatGLM Api Key.
### `CHATGLM_URL` (optional)
ChatGLM Api Url.
### `HIDE_USER_API_KEY` (optional)
> Default: Empty
@@ -325,9 +345,9 @@ To control custom models, use `+` to add a custom model, use `-` to hide a model
User `-all` to disable all default models, `+all` to enable all default models.
For Azure: use `modelName@azure=deploymentName` to customize model name and deployment name.
> Example: `+gpt-3.5-turbo@azure=gpt35` will show option `gpt35(Azure)` in model list.
> If you only can use Azure model, `-all,+gpt-3.5-turbo@azure=gpt35` will `gpt35(Azure)` the only option in model list.
For Azure: use `modelName@Azure=deploymentName` to customize model name and deployment name.
> Example: `+gpt-3.5-turbo@Azure=gpt35` will show option `gpt35(Azure)` in model list.
> If you only can use Azure model, `-all,+gpt-3.5-turbo@Azure=gpt35` will `gpt35(Azure)` the only option in model list.
For ByteDance: use `modelName@bytedance=deploymentName` to customize model name and deployment name.
> Example: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx` will show option `Doubao-lite-4k(ByteDance)` in model list.
@@ -336,7 +356,14 @@ For ByteDance: use `modelName@bytedance=deploymentName` to customize model name
Change default model
### `WHITE_WEBDEV_ENDPOINTS` (optional)
### `VISION_MODELS` (optional)
> Default: Empty
> Example: `gpt-4-vision,claude-3-opus,my-custom-model` means add vision capabilities to these models in addition to the default pattern matches (which detect models containing keywords like "vision", "claude-3", "gemini-1.5", etc).
Add additional models to have vision capabilities, beyond the default pattern matching. Multiple models should be separated by commas.
### `WHITE_WEBDAV_ENDPOINTS` (optional)
You can use this option if you want to increase the number of webdav service addresses you are allowed to access, as required by the format
- Each address must be a complete endpoint
@@ -388,6 +415,9 @@ yarn dev
> [简体中文 > 如何部署到私人服务器](./README_CN.md#部署)
### BT Install
> [简体中文 > 如何通过宝塔一键部署](./docs/bt-cn.md)
### Docker (Recommended)
```shell

View File

@@ -8,7 +8,7 @@
一键免费部署你的私人 ChatGPT 网页应用,支持 GPT3, GPT4 & Gemini Pro 模型。
[企业版](#%E4%BC%81%E4%B8%9A%E7%89%88) /[演示 Demo](https://chat-gpt-next-web.vercel.app/) / [反馈 Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [加入 Discord](https://discord.gg/zrhvHCr79N)
[NextChatAI](https://nextchat.dev/chat?utm_source=readme) / [企业版](#%E4%BC%81%E4%B8%9A%E7%89%88) / [演示 Demo](https://chat-gpt-next-web.vercel.app/) / [反馈 Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [加入 Discord](https://discord.gg/zrhvHCr79N)
[<img src="https://vercel.com/button" alt="Deploy on Zeabur" height="30">](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FChatGPTNextWeb%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=nextchat&repository-name=NextChat) [<img src="https://zeabur.com/button.svg" alt="Deploy on Zeabur" height="30">](https://zeabur.com/templates/ZBUEFA) [<img src="https://gitpod.io/button/open-in-gitpod.svg" alt="Open in Gitpod" height="30">](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
@@ -54,7 +54,7 @@
### 打开自动更新
> 如果你遇到了 Upstream Sync 执行错误,请手动 Sync Fork 一次!
> 如果你遇到了 Upstream Sync 执行错误,请[手动 Sync Fork 一次](./README_CN.md#手动更新代码)
当你 fork 项目之后,由于 Github 的限制,需要手动去你 fork 后的项目的 Actions 页面启用 Workflows并启用 Upstream Sync Action启用之后即可开启每小时定时自动更新
@@ -184,6 +184,13 @@ ByteDance Api Url.
讯飞星火Api Secret.
### `CHATGLM_API_KEY` (可选)
ChatGLM Api Key.
### `CHATGLM_URL` (可选)
ChatGLM Api Url.
### `HIDE_USER_API_KEY` (可选)
@@ -202,7 +209,7 @@ ByteDance Api Url.
如果你想禁用从链接解析预制设置,将此环境变量设置为 1 即可。
### `WHITE_WEBDEV_ENDPOINTS` (可选)
### `WHITE_WEBDAV_ENDPOINTS` (可选)
如果你想增加允许访问的webdav服务地址可以使用该选项格式要求
- 每一个地址必须是一个完整的 endpoint
@@ -216,9 +223,9 @@ ByteDance Api Url.
用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名=展示名` 来自定义模型的展示名,用英文逗号隔开。
在Azure的模式下支持使用`modelName@azure=deploymentName`的方式配置模型名称和部署名称(deploy-name)
> 示例:`+gpt-3.5-turbo@azure=gpt35`这个配置会在模型列表显示一个`gpt35(Azure)`的选项。
> 如果你只能使用Azure模式那么设置 `-all,+gpt-3.5-turbo@azure=gpt35` 则可以让对话的默认使用 `gpt35(Azure)`
在Azure的模式下支持使用`modelName@Azure=deploymentName`的方式配置模型名称和部署名称(deploy-name)
> 示例:`+gpt-3.5-turbo@Azure=gpt35`这个配置会在模型列表显示一个`gpt35(Azure)`的选项。
> 如果你只能使用Azure模式那么设置 `-all,+gpt-3.5-turbo@Azure=gpt35` 则可以让对话的默认使用 `gpt35(Azure)`
在ByteDance的模式下支持使用`modelName@bytedance=deploymentName`的方式配置模型名称和部署名称(deploy-name)
> 示例: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx`这个配置会在模型列表显示一个`Doubao-lite-4k(ByteDance)`的选项
@@ -228,6 +235,13 @@ ByteDance Api Url.
更改默认模型
### `VISION_MODELS` (可选)
> 默认值:空
> 示例:`gpt-4-vision,claude-3-opus,my-custom-model` 表示为这些模型添加视觉能力,作为对默认模式匹配的补充(默认会检测包含"vision"、"claude-3"、"gemini-1.5"等关键词的模型)。
在默认模式匹配之外,添加更多具有视觉能力的模型。多个模型用逗号分隔。
### `DEFAULT_INPUT_TEMPLATE` (可选)
自定义默认的 template用于初始化『设置』中的『用户输入预处理』配置项
@@ -264,6 +278,9 @@ BASE_URL=https://b.nextweb.fun/api/proxy
## 部署
### 宝塔面板部署
> [简体中文 > 如何通过宝塔一键部署](./docs/bt-cn.md)
### 容器部署 (推荐)
> Docker 版本需要在 20 及其以上,否则会提示找不到镜像。

View File

@@ -5,7 +5,7 @@
ワンクリックで無料であなた専用の ChatGPT ウェブアプリをデプロイ。GPT3、GPT4 & Gemini Pro モデルをサポート。
[企業版](#企業版) / [デモ](https://chat-gpt-next-web.vercel.app/) / [フィードバック](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [Discordに参加](https://discord.gg/zrhvHCr79N)
[NextChatAI](https://nextchat.dev/chat?utm_source=readme) / [企業版](#企業版) / [デモ](https://chat-gpt-next-web.vercel.app/) / [フィードバック](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [Discordに参加](https://discord.gg/zrhvHCr79N)
[<img src="https://vercel.com/button" alt="Zeaburでデプロイ" height="30">](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FChatGPTNextWeb%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=nextchat&repository-name=NextChat) [<img src="https://zeabur.com/button.svg" alt="Zeaburでデプロイ" height="30">](https://zeabur.com/templates/ZBUEFA) [<img src="https://gitpod.io/button/open-in-gitpod.svg" alt="Gitpodで開く" height="30">](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
@@ -54,7 +54,7 @@
### 自動更新を開く
> Upstream Sync の実行エラーが発生した場合は、手動で Sync Fork してください!
> Upstream Sync の実行エラーが発生した場合は、[手動で Sync Fork](./README_JA.md#手動でコードを更新する) してください!
プロジェクトを fork した後、GitHub の制限により、fork 後のプロジェクトの Actions ページで Workflows を手動で有効にし、Upstream Sync Action を有効にする必要があります。有効化後、毎時の定期自動更新が可能になります:
@@ -193,7 +193,7 @@ ByteDance API の URL。
リンクからのプリセット設定解析を無効にしたい場合は、この環境変数を 1 に設定します。
### `WHITE_WEBDEV_ENDPOINTS` (オプション)
### `WHITE_WEBDAV_ENDPOINTS` (オプション)
アクセス許可を与える WebDAV サービスのアドレスを追加したい場合、このオプションを使用します。フォーマット要件:
- 各アドレスは完全なエンドポイントでなければなりません。
@@ -207,8 +207,8 @@ ByteDance API の URL。
モデルリストを管理します。`+` でモデルを追加し、`-` でモデルを非表示にし、`モデル名=表示名` でモデルの表示名をカスタマイズし、カンマで区切ります。
Azure モードでは、`modelName@azure=deploymentName` 形式でモデル名とデプロイ名deploy-nameを設定できます。
> 例:`+gpt-3.5-turbo@azure=gpt35` この設定でモデルリストに `gpt35(Azure)` のオプションが表示されます。
Azure モードでは、`modelName@Azure=deploymentName` 形式でモデル名とデプロイ名deploy-nameを設定できます。
> 例:`+gpt-3.5-turbo@Azure=gpt35` この設定でモデルリストに `gpt35(Azure)` のオプションが表示されます。
ByteDance モードでは、`modelName@bytedance=deploymentName` 形式でモデル名とデプロイ名deploy-nameを設定できます。
> 例: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx` この設定でモデルリストに `Doubao-lite-4k(ByteDance)` のオプションが表示されます。
@@ -217,6 +217,13 @@ ByteDance モードでは、`modelName@bytedance=deploymentName` 形式でモデ
デフォルトのモデルを変更します。
### `VISION_MODELS` (オプション)
> デフォルト:空
> 例:`gpt-4-vision,claude-3-opus,my-custom-model` は、これらのモデルにビジョン機能を追加します。これはデフォルトのパターンマッチング("vision"、"claude-3"、"gemini-1.5"などのキーワードを含むモデルを検出)に加えて適用されます。
デフォルトのパターンマッチングに加えて、追加のモデルにビジョン機能を付与します。複数のモデルはカンマで区切ります。
### `DEFAULT_INPUT_TEMPLATE` (オプション)
『設定』の『ユーザー入力前処理』の初期設定に使用するテンプレートをカスタマイズします。

View File

@@ -1,5 +1,5 @@
import { ApiPath } from "@/app/constant";
import { NextRequest, NextResponse } from "next/server";
import { NextRequest } from "next/server";
import { handle as openaiHandler } from "../../openai";
import { handle as azureHandler } from "../../azure";
import { handle as googleHandler } from "../../google";
@@ -10,6 +10,10 @@ import { handle as alibabaHandler } from "../../alibaba";
import { handle as moonshotHandler } from "../../moonshot";
import { handle as stabilityHandler } from "../../stability";
import { handle as iflytekHandler } from "../../iflytek";
import { handle as xaiHandler } from "../../xai";
import { handle as chatglmHandler } from "../../glm";
import { handle as proxyHandler } from "../../proxy";
async function handle(
req: NextRequest,
{ params }: { params: { provider: string; path: string[] } },
@@ -36,8 +40,14 @@ async function handle(
return stabilityHandler(req, { params });
case ApiPath.Iflytek:
return iflytekHandler(req, { params });
default:
case ApiPath.XAI:
return xaiHandler(req, { params });
case ApiPath.ChatGLM:
return chatglmHandler(req, { params });
case ApiPath.OpenAI:
return openaiHandler(req, { params });
default:
return proxyHandler(req, { params });
}
}

View File

@@ -1,6 +1,5 @@
import { getServerSideConfig } from "@/app/config/server";
import {
Alibaba,
ALIBABA_BASE_URL,
ApiPath,
ModelProvider,
@@ -10,7 +9,6 @@ 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();

View File

@@ -3,7 +3,6 @@ import {
ANTHROPIC_BASE_URL,
Anthropic,
ApiPath,
DEFAULT_MODELS,
ServiceProvider,
ModelProvider,
} from "@/app/constant";
@@ -98,6 +97,7 @@ async function request(req: NextRequest) {
headers: {
"Content-Type": "application/json",
"Cache-Control": "no-store",
"anthropic-dangerous-direct-browser-access": "true",
[authHeaderName]: authValue,
"anthropic-version":
req.headers.get("anthropic-version") ||

View File

@@ -92,6 +92,12 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) {
systemApiKey =
serverConfig.iflytekApiKey + ":" + serverConfig.iflytekApiSecret;
break;
case ModelProvider.XAI:
systemApiKey = serverConfig.xaiApiKey;
break;
case ModelProvider.ChatGLM:
systemApiKey = serverConfig.chatglmApiKey;
break;
case ModelProvider.GPT:
default:
if (req.nextUrl.pathname.includes("azure/deployments")) {

View File

@@ -1,4 +1,3 @@
import { getServerSideConfig } from "@/app/config/server";
import { ModelProvider } from "@/app/constant";
import { prettyObject } from "@/app/utils/format";
import { NextRequest, NextResponse } from "next/server";

View File

@@ -3,7 +3,6 @@ import {
BAIDU_BASE_URL,
ApiPath,
ModelProvider,
BAIDU_OATUH_URL,
ServiceProvider,
} from "@/app/constant";
import { prettyObject } from "@/app/utils/format";

View File

@@ -1,13 +1,8 @@
import { NextRequest, NextResponse } from "next/server";
import { getServerSideConfig } from "../config/server";
import {
DEFAULT_MODELS,
OPENAI_BASE_URL,
GEMINI_BASE_URL,
ServiceProvider,
} from "../constant";
import { isModelAvailableInServer } from "../utils/model";
import { OPENAI_BASE_URL, ServiceProvider } from "../constant";
import { cloudflareAIGatewayUrl } from "../utils/cloudflare";
import { getModelProvider, isModelAvailableInServer } from "../utils/model";
const serverConfig = getServerSideConfig();
@@ -32,10 +27,7 @@ export async function requestOpenai(req: NextRequest) {
authHeaderName = "Authorization";
}
let path = `${req.nextUrl.pathname}${req.nextUrl.search}`.replaceAll(
"/api/openai/",
"",
);
let path = `${req.nextUrl.pathname}`.replaceAll("/api/openai/", "");
let baseUrl =
(isAzure ? serverConfig.azureUrl : serverConfig.baseUrl) || OPENAI_BASE_URL;
@@ -79,7 +71,7 @@ export async function requestOpenai(req: NextRequest) {
.filter((v) => !!v && !v.startsWith("-") && v.includes(modelName))
.forEach((m) => {
const [fullName, displayName] = m.split("=");
const [_, providerName] = fullName.split("@");
const [_, providerName] = getModelProvider(fullName);
if (providerName === "azure" && !displayName) {
const [_, deployId] = (serverConfig?.azureUrl ?? "").split(
"deployments/",

129
app/api/glm.ts Normal file
View File

@@ -0,0 +1,129 @@
import { getServerSideConfig } from "@/app/config/server";
import {
CHATGLM_BASE_URL,
ApiPath,
ModelProvider,
ServiceProvider,
} from "@/app/constant";
import { prettyObject } from "@/app/utils/format";
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/app/api/auth";
import { isModelAvailableInServer } from "@/app/utils/model";
const serverConfig = getServerSideConfig();
export async function handle(
req: NextRequest,
{ params }: { params: { path: string[] } },
) {
console.log("[GLM Route] params ", params);
if (req.method === "OPTIONS") {
return NextResponse.json({ body: "OK" }, { status: 200 });
}
const authResult = auth(req, ModelProvider.ChatGLM);
if (authResult.error) {
return NextResponse.json(authResult, {
status: 401,
});
}
try {
const response = await request(req);
return response;
} catch (e) {
console.error("[GLM] ", e);
return NextResponse.json(prettyObject(e));
}
}
async function request(req: NextRequest) {
const controller = new AbortController();
// alibaba use base url or just remove the path
let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.ChatGLM, "");
let baseUrl = serverConfig.chatglmUrl || CHATGLM_BASE_URL;
if (!baseUrl.startsWith("http")) {
baseUrl = `https://${baseUrl}`;
}
if (baseUrl.endsWith("/")) {
baseUrl = baseUrl.slice(0, -1);
}
console.log("[Proxy] ", path);
console.log("[Base Url]", baseUrl);
const timeoutId = setTimeout(
() => {
controller.abort();
},
10 * 60 * 1000,
);
const fetchUrl = `${baseUrl}${path}`;
console.log("[Fetch Url] ", fetchUrl);
const fetchOptions: RequestInit = {
headers: {
"Content-Type": "application/json",
Authorization: req.headers.get("Authorization") ?? "",
},
method: req.method,
body: req.body,
redirect: "manual",
// @ts-ignore
duplex: "half",
signal: controller.signal,
};
// #1815 try to refuse some request to some models
if (serverConfig.customModels && req.body) {
try {
const clonedBody = await req.text();
fetchOptions.body = clonedBody;
const jsonBody = JSON.parse(clonedBody) as { model?: string };
// not undefined and is false
if (
isModelAvailableInServer(
serverConfig.customModels,
jsonBody?.model as string,
ServiceProvider.ChatGLM as string,
)
) {
return NextResponse.json(
{
error: true,
message: `you are not allowed to use ${jsonBody?.model} model`,
},
{
status: 403,
},
);
}
} catch (e) {
console.error(`[GLM] filter`, e);
}
}
try {
const res = await fetch(fetchUrl, fetchOptions);
// to prevent browser prompt for credentials
const newHeaders = new Headers(res.headers);
newHeaders.delete("www-authenticate");
// to disable nginx buffering
newHeaders.set("X-Accel-Buffering", "no");
return new Response(res.body, {
status: res.status,
statusText: res.statusText,
headers: newHeaders,
});
} finally {
clearTimeout(timeoutId);
}
}

View File

@@ -1,12 +1,7 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "./auth";
import { getServerSideConfig } from "@/app/config/server";
import {
ApiPath,
GEMINI_BASE_URL,
Google,
ModelProvider,
} from "@/app/constant";
import { ApiPath, GEMINI_BASE_URL, ModelProvider } from "@/app/constant";
import { prettyObject } from "@/app/utils/format";
const serverConfig = getServerSideConfig();
@@ -28,7 +23,8 @@ export async function handle(
});
}
const bearToken = req.headers.get("Authorization") ?? "";
const bearToken =
req.headers.get("x-goog-api-key") || req.headers.get("Authorization") || "";
const token = bearToken.trim().replaceAll("Bearer ", "").trim();
const apiKey = token ? token : serverConfig.googleApiKey;
@@ -96,8 +92,8 @@ async function request(req: NextRequest, apiKey: string) {
},
10 * 60 * 1000,
);
const fetchUrl = `${baseUrl}${path}?key=${apiKey}${
req?.nextUrl?.searchParams?.get("alt") === "sse" ? "&alt=sse" : ""
const fetchUrl = `${baseUrl}${path}${
req?.nextUrl?.searchParams?.get("alt") === "sse" ? "?alt=sse" : ""
}`;
console.log("[Fetch Url] ", fetchUrl);
@@ -105,6 +101,9 @@ async function request(req: NextRequest, apiKey: string) {
headers: {
"Content-Type": "application/json",
"Cache-Control": "no-store",
"x-goog-api-key":
req.headers.get("x-goog-api-key") ||
(req.headers.get("Authorization") ?? "").replace("Bearer ", ""),
},
method: req.method,
body: req.body,

View File

@@ -1,6 +1,5 @@
import { getServerSideConfig } from "@/app/config/server";
import {
Iflytek,
IFLYTEK_BASE_URL,
ApiPath,
ModelProvider,
@@ -10,7 +9,6 @@ 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();

View File

@@ -1,6 +1,5 @@
import { getServerSideConfig } from "@/app/config/server";
import {
Moonshot,
MOONSHOT_BASE_URL,
ApiPath,
ModelProvider,
@@ -10,7 +9,6 @@ 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();

View File

@@ -6,14 +6,16 @@ import { NextRequest, NextResponse } from "next/server";
import { auth } from "./auth";
import { requestOpenai } from "./common";
const ALLOWD_PATH = new Set(Object.values(OpenaiPath));
const ALLOWED_PATH = new Set(Object.values(OpenaiPath));
function getModels(remoteModelRes: OpenAIListModelResponse) {
const config = getServerSideConfig();
if (config.disableGPT4) {
remoteModelRes.data = remoteModelRes.data.filter(
(m) => !m.id.startsWith("gpt-4"),
(m) =>
!(m.id.startsWith("gpt-4") || m.id.startsWith("chatgpt-4o") || m.id.startsWith("o1")) ||
m.id.startsWith("gpt-4o-mini"),
);
}
@@ -32,7 +34,7 @@ export async function handle(
const subpath = params.path.join("/");
if (!ALLOWD_PATH.has(subpath)) {
if (!ALLOWED_PATH.has(subpath)) {
console.log("[OpenAI Route] forbidden path ", subpath);
return NextResponse.json(
{

89
app/api/proxy.ts Normal file
View File

@@ -0,0 +1,89 @@
import { NextRequest, NextResponse } from "next/server";
import { getServerSideConfig } from "@/app/config/server";
export async function handle(
req: NextRequest,
{ params }: { params: { path: string[] } },
) {
console.log("[Proxy Route] params ", params);
if (req.method === "OPTIONS") {
return NextResponse.json({ body: "OK" }, { status: 200 });
}
const serverConfig = getServerSideConfig();
// remove path params from searchParams
req.nextUrl.searchParams.delete("path");
req.nextUrl.searchParams.delete("provider");
const subpath = params.path.join("/");
const fetchUrl = `${req.headers.get(
"x-base-url",
)}/${subpath}?${req.nextUrl.searchParams.toString()}`;
const skipHeaders = ["connection", "host", "origin", "referer", "cookie"];
const headers = new Headers(
Array.from(req.headers.entries()).filter((item) => {
if (
item[0].indexOf("x-") > -1 ||
item[0].indexOf("sec-") > -1 ||
skipHeaders.includes(item[0])
) {
return false;
}
return true;
}),
);
// if dalle3 use openai api key
const baseUrl = req.headers.get("x-base-url");
if (baseUrl?.includes("api.openai.com")) {
if (!serverConfig.apiKey) {
return NextResponse.json(
{ error: "OpenAI API key not configured" },
{ status: 500 },
);
}
headers.set("Authorization", `Bearer ${serverConfig.apiKey}`);
}
const controller = new AbortController();
const fetchOptions: RequestInit = {
headers,
method: req.method,
body: req.body,
// to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body
redirect: "manual",
// @ts-ignore
duplex: "half",
signal: controller.signal,
};
const timeoutId = setTimeout(
() => {
controller.abort();
},
10 * 60 * 1000,
);
try {
const res = await fetch(fetchUrl, fetchOptions);
// to prevent browser prompt for credentials
const newHeaders = new Headers(res.headers);
newHeaders.delete("www-authenticate");
// to disable nginx buffering
newHeaders.set("X-Accel-Buffering", "no");
// The latest version of the OpenAI API forced the content-encoding to be "br" in json response
// So if the streaming is disabled, we need to remove the content-encoding header
// Because Vercel uses gzip to compress the response, if we don't remove the content-encoding header
// The browser will try to decode the response with brotli and fail
newHeaders.delete("content-encoding");
return new Response(res.body, {
status: res.status,
statusText: res.statusText,
headers: newHeaders,
});
} finally {
clearTimeout(timeoutId);
}
}

View File

@@ -1,15 +1,8 @@
import { getServerSideConfig } from "@/app/config/server";
import {
TENCENT_BASE_URL,
ApiPath,
ModelProvider,
ServiceProvider,
Tencent,
} from "@/app/constant";
import { TENCENT_BASE_URL, ModelProvider } from "@/app/constant";
import { prettyObject } from "@/app/utils/format";
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/app/api/auth";
import { isModelAvailableInServer } from "@/app/utils/model";
import { getHeader } from "@/app/utils/tencent";
const serverConfig = getServerSideConfig();

View File

@@ -6,7 +6,7 @@ const config = getServerSideConfig();
const mergedAllowedWebDavEndpoints = [
...internalAllowedWebDavEndpoints,
...config.allowedWebDevEndpoints,
...config.allowedWebDavEndpoints,
].filter((domain) => Boolean(domain.trim()));
const normalizeUrl = (url: string) => {

128
app/api/xai.ts Normal file
View File

@@ -0,0 +1,128 @@
import { getServerSideConfig } from "@/app/config/server";
import {
XAI_BASE_URL,
ApiPath,
ModelProvider,
ServiceProvider,
} from "@/app/constant";
import { prettyObject } from "@/app/utils/format";
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/app/api/auth";
import { isModelAvailableInServer } from "@/app/utils/model";
const serverConfig = getServerSideConfig();
export async function handle(
req: NextRequest,
{ params }: { params: { path: string[] } },
) {
console.log("[XAI Route] params ", params);
if (req.method === "OPTIONS") {
return NextResponse.json({ body: "OK" }, { status: 200 });
}
const authResult = auth(req, ModelProvider.XAI);
if (authResult.error) {
return NextResponse.json(authResult, {
status: 401,
});
}
try {
const response = await request(req);
return response;
} catch (e) {
console.error("[XAI] ", e);
return NextResponse.json(prettyObject(e));
}
}
async function request(req: NextRequest) {
const controller = new AbortController();
// alibaba use base url or just remove the path
let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.XAI, "");
let baseUrl = serverConfig.xaiUrl || XAI_BASE_URL;
if (!baseUrl.startsWith("http")) {
baseUrl = `https://${baseUrl}`;
}
if (baseUrl.endsWith("/")) {
baseUrl = baseUrl.slice(0, -1);
}
console.log("[Proxy] ", path);
console.log("[Base Url]", baseUrl);
const timeoutId = setTimeout(
() => {
controller.abort();
},
10 * 60 * 1000,
);
const fetchUrl = `${baseUrl}${path}`;
const fetchOptions: RequestInit = {
headers: {
"Content-Type": "application/json",
Authorization: req.headers.get("Authorization") ?? "",
},
method: req.method,
body: req.body,
redirect: "manual",
// @ts-ignore
duplex: "half",
signal: controller.signal,
};
// #1815 try to refuse some request to some models
if (serverConfig.customModels && req.body) {
try {
const clonedBody = await req.text();
fetchOptions.body = clonedBody;
const jsonBody = JSON.parse(clonedBody) as { model?: string };
// not undefined and is false
if (
isModelAvailableInServer(
serverConfig.customModels,
jsonBody?.model as string,
ServiceProvider.XAI as string,
)
) {
return NextResponse.json(
{
error: true,
message: `you are not allowed to use ${jsonBody?.model} model`,
},
{
status: 403,
},
);
}
} catch (e) {
console.error(`[XAI] filter`, e);
}
}
try {
const res = await fetch(fetchUrl, fetchOptions);
// to prevent browser prompt for credentials
const newHeaders = new Headers(res.headers);
newHeaders.delete("www-authenticate");
// to disable nginx buffering
newHeaders.set("X-Accel-Buffering", "no");
return new Response(res.body, {
status: res.status,
statusText: res.statusText,
headers: newHeaders,
});
} finally {
clearTimeout(timeoutId);
}
}

View File

@@ -1,11 +1,16 @@
import { getClientConfig } from "../config/client";
import {
ACCESS_CODE_PREFIX,
Azure,
ModelProvider,
ServiceProvider,
} from "../constant";
import { ChatMessage, ModelType, useAccessStore, useChatStore } from "../store";
import {
ChatMessageTool,
ChatMessage,
ModelType,
useAccessStore,
useChatStore,
} from "../store";
import { ChatGPTApi, DalleRequestPayload } from "./platforms/openai";
import { GeminiProApi } from "./platforms/google";
import { ClaudeApi } from "./platforms/anthropic";
@@ -15,11 +20,14 @@ import { QwenApi } from "./platforms/alibaba";
import { HunyuanApi } from "./platforms/tencent";
import { MoonshotApi } from "./platforms/moonshot";
import { SparkApi } from "./platforms/iflytek";
import { XAIApi } from "./platforms/xai";
import { ChatGLMApi } from "./platforms/glm";
export const ROLES = ["system", "user", "assistant"] as const;
export type MessageRole = (typeof ROLES)[number];
export const Models = ["gpt-3.5-turbo", "gpt-4"] as const;
export const TTSModels = ["tts-1", "tts-1-hd"] as const;
export type ChatModel = ModelType;
export interface MultimodalContent {
@@ -44,6 +52,17 @@ export interface LLMConfig {
presence_penalty?: number;
frequency_penalty?: number;
size?: DalleRequestPayload["size"];
quality?: DalleRequestPayload["quality"];
style?: DalleRequestPayload["style"];
}
export interface SpeechOptions {
model: string;
input: string;
voice: string;
response_format?: string;
speed?: number;
onController?: (controller: AbortController) => void;
}
export interface ChatOptions {
@@ -51,9 +70,11 @@ export interface ChatOptions {
config: LLMConfig;
onUpdate?: (message: string, chunk: string) => void;
onFinish: (message: string) => void;
onFinish: (message: string, responseRes: Response) => void;
onError?: (err: Error) => void;
onController?: (controller: AbortController) => void;
onBeforeTool?: (tool: ChatMessageTool) => void;
onAfterTool?: (tool: ChatMessageTool) => void;
}
export interface LLMUsage {
@@ -78,6 +99,7 @@ export interface LLMModelProvider {
export abstract class LLMApi {
abstract chat(options: ChatOptions): Promise<void>;
abstract speech(options: SpeechOptions): Promise<ArrayBuffer>;
abstract usage(): Promise<LLMUsage>;
abstract models(): Promise<LLMModel[]>;
}
@@ -132,6 +154,12 @@ export class ClientApi {
case ModelProvider.Iflytek:
this.llm = new SparkApi();
break;
case ModelProvider.XAI:
this.llm = new XAIApi();
break;
case ModelProvider.ChatGLM:
this.llm = new ChatGLMApi();
break;
default:
this.llm = new ChatGPTApi();
}
@@ -196,19 +224,22 @@ export function validString(x: string): boolean {
return x?.length > 0;
}
export function getHeaders() {
export function getHeaders(ignoreHeaders: boolean = false) {
const accessStore = useAccessStore.getState();
const chatStore = useChatStore.getState();
const headers: Record<string, string> = {
"Content-Type": "application/json",
Accept: "application/json",
};
let headers: Record<string, string> = {};
if (!ignoreHeaders) {
headers = {
"Content-Type": "application/json",
Accept: "application/json",
};
}
const clientConfig = getClientConfig();
function getConfig() {
const modelConfig = chatStore.currentSession().mask.modelConfig;
const isGoogle = modelConfig.providerName == ServiceProvider.Google;
const isGoogle = modelConfig.providerName === ServiceProvider.Google;
const isAzure = modelConfig.providerName === ServiceProvider.Azure;
const isAnthropic = modelConfig.providerName === ServiceProvider.Anthropic;
const isBaidu = modelConfig.providerName == ServiceProvider.Baidu;
@@ -216,6 +247,8 @@ export function getHeaders() {
const isAlibaba = modelConfig.providerName === ServiceProvider.Alibaba;
const isMoonshot = modelConfig.providerName === ServiceProvider.Moonshot;
const isIflytek = modelConfig.providerName === ServiceProvider.Iflytek;
const isXAI = modelConfig.providerName === ServiceProvider.XAI;
const isChatGLM = modelConfig.providerName === ServiceProvider.ChatGLM;
const isEnabledAccessControl = accessStore.enabledAccessControl();
const apiKey = isGoogle
? accessStore.googleApiKey
@@ -229,6 +262,10 @@ export function getHeaders() {
? accessStore.alibabaApiKey
: isMoonshot
? accessStore.moonshotApiKey
: isXAI
? accessStore.xaiApiKey
: isChatGLM
? accessStore.chatglmApiKey
: isIflytek
? accessStore.iflytekApiKey && accessStore.iflytekApiSecret
? accessStore.iflytekApiKey + ":" + accessStore.iflytekApiSecret
@@ -243,13 +280,21 @@ export function getHeaders() {
isAlibaba,
isMoonshot,
isIflytek,
isXAI,
isChatGLM,
apiKey,
isEnabledAccessControl,
};
}
function getAuthHeader(): string {
return isAzure ? "api-key" : isAnthropic ? "x-api-key" : "Authorization";
return isAzure
? "api-key"
: isAnthropic
? "x-api-key"
: isGoogle
? "x-goog-api-key"
: "Authorization";
}
const {
@@ -260,14 +305,15 @@ export function getHeaders() {
apiKey,
isEnabledAccessControl,
} = getConfig();
// when using google api in app, not set auth header
if (isGoogle && clientConfig?.isApp) return headers;
// when using baidu api in app, not set auth header
if (isBaidu && clientConfig?.isApp) return headers;
const authHeader = getAuthHeader();
const bearerToken = getBearerToken(apiKey, isAzure || isAnthropic);
const bearerToken = getBearerToken(
apiKey,
isAzure || isAnthropic || isGoogle,
);
if (bearerToken) {
headers[authHeader] = bearerToken;
@@ -298,6 +344,10 @@ export function getClientApi(provider: ServiceProvider): ClientApi {
return new ClientApi(ModelProvider.Moonshot);
case ServiceProvider.Iflytek:
return new ClientApi(ModelProvider.Iflytek);
case ServiceProvider.XAI:
return new ClientApi(ModelProvider.XAI);
case ServiceProvider.ChatGLM:
return new ClientApi(ModelProvider.ChatGLM);
default:
return new ClientApi(ModelProvider.GPT);
}

View File

@@ -12,6 +12,7 @@ import {
getHeaders,
LLMApi,
LLMModel,
SpeechOptions,
MultimodalContent,
} from "../api";
import Locale from "../../locales";
@@ -22,6 +23,7 @@ import {
import { prettyObject } from "@/app/utils/format";
import { getClientConfig } from "@/app/config/client";
import { getMessageTextContent } from "@/app/utils";
import { fetch } from "@/app/utils/stream";
export interface OpenAIListModelResponse {
object: string;
@@ -83,6 +85,10 @@ export class QwenApi implements LLMApi {
return res?.output?.choices?.at(0)?.message?.content ?? "";
}
speech(options: SpeechOptions): Promise<ArrayBuffer> {
throw new Error("Method not implemented.");
}
async chat(options: ChatOptions) {
const messages = options.messages.map((v) => ({
role: v.role,
@@ -137,6 +143,7 @@ export class QwenApi implements LLMApi {
let responseText = "";
let remainText = "";
let finished = false;
let responseRes: Response;
// animate response to make it looks smooth
function animateResponseText() {
@@ -166,13 +173,14 @@ export class QwenApi implements LLMApi {
const finish = () => {
if (!finished) {
finished = true;
options.onFinish(responseText + remainText);
options.onFinish(responseText + remainText, responseRes);
}
};
controller.signal.onabort = finish;
fetchEventSource(chatPath, {
fetch: fetch as any,
...chatPayload,
async onopen(res) {
clearTimeout(requestTimeoutId);
@@ -181,6 +189,7 @@ export class QwenApi implements LLMApi {
"[Alibaba] request response content type: ",
contentType,
);
responseRes = res;
if (contentType?.startsWith("text/plain")) {
responseText = await res.clone().text();
@@ -247,7 +256,7 @@ export class QwenApi implements LLMApi {
const resJson = await res.json();
const message = this.extractMessage(resJson);
options.onFinish(message);
options.onFinish(message, res);
}
} catch (e) {
console.log("[Request] failed to make a chat request", e);

View File

@@ -1,18 +1,19 @@
import { ACCESS_CODE_PREFIX, Anthropic, ApiPath } from "@/app/constant";
import { ChatOptions, getHeaders, LLMApi, MultimodalContent } from "../api";
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
import { getClientConfig } from "@/app/config/client";
import { DEFAULT_API_HOST } from "@/app/constant";
import { Anthropic, ApiPath } from "@/app/constant";
import { ChatOptions, getHeaders, LLMApi, SpeechOptions } from "../api";
import {
EventStreamContentType,
fetchEventSource,
} from "@fortaine/fetch-event-source";
import Locale from "../../locales";
import { prettyObject } from "@/app/utils/format";
useAccessStore,
useAppConfig,
useChatStore,
usePluginStore,
ChatMessageTool,
} from "@/app/store";
import { getClientConfig } from "@/app/config/client";
import { ANTHROPIC_BASE_URL } from "@/app/constant";
import { getMessageTextContent, isVisionModel } from "@/app/utils";
import { preProcessImageContent } from "@/app/utils/chat";
import { preProcessImageContent, stream } from "@/app/utils/chat";
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
import { RequestPayload } from "./openai";
import { fetch } from "@/app/utils/stream";
export type MultiBlockContent = {
type: "image" | "text";
@@ -73,6 +74,10 @@ const ClaudeMapper = {
const keys = ["claude-2, claude-instant-1"];
export class ClaudeApi implements LLMApi {
speech(options: SpeechOptions): Promise<ArrayBuffer> {
throw new Error("Method not implemented.");
}
extractMessage(res: any) {
console.log("[Response] claude response: ", res);
@@ -191,120 +196,135 @@ export class ClaudeApi implements LLMApi {
const controller = new AbortController();
options.onController?.(controller);
const payload = {
method: "POST",
body: JSON.stringify(requestBody),
signal: controller.signal,
headers: {
...getHeaders(), // get common headers
"anthropic-version": accessStore.anthropicApiVersion,
// do not send `anthropicApiKey` in browser!!!
// Authorization: getAuthKey(accessStore.anthropicApiKey),
},
};
if (shouldStream) {
try {
const context = {
text: "",
finished: false,
};
const finish = () => {
if (!context.finished) {
options.onFinish(context.text);
context.finished = true;
}
};
controller.signal.onabort = finish;
fetchEventSource(path, {
...payload,
async onopen(res) {
const contentType = res.headers.get("content-type");
console.log("response content type: ", contentType);
if (contentType?.startsWith("text/plain")) {
context.text = await res.clone().text();
return finish();
}
if (
!res.ok ||
!res.headers
.get("content-type")
?.startsWith(EventStreamContentType) ||
res.status !== 200
) {
const responseTexts = [context.text];
let extraInfo = await res.clone().text();
try {
const resJson = await res.clone().json();
extraInfo = prettyObject(resJson);
} catch {}
if (res.status === 401) {
responseTexts.push(Locale.Error.Unauthorized);
}
if (extraInfo) {
responseTexts.push(extraInfo);
}
context.text = responseTexts.join("\n\n");
return finish();
}
},
onmessage(msg) {
let chunkJson:
| undefined
| {
type: "content_block_delta" | "content_block_stop";
delta?: {
type: "text_delta";
text: string;
};
index: number;
let index = -1;
const [tools, funcs] = usePluginStore
.getState()
.getAsTools(
useChatStore.getState().currentSession().mask?.plugin || [],
);
return stream(
path,
requestBody,
{
...getHeaders(),
"anthropic-version": accessStore.anthropicApiVersion,
},
// @ts-ignore
tools.map((tool) => ({
name: tool?.function?.name,
description: tool?.function?.description,
input_schema: tool?.function?.parameters,
})),
funcs,
controller,
// parseSSE
(text: string, runTools: ChatMessageTool[]) => {
// console.log("parseSSE", text, runTools);
let chunkJson:
| undefined
| {
type: "content_block_delta" | "content_block_stop";
content_block?: {
type: "tool_use";
id: string;
name: string;
};
try {
chunkJson = JSON.parse(msg.data);
} catch (e) {
console.error("[Response] parse error", msg.data);
}
delta?: {
type: "text_delta" | "input_json_delta";
text?: string;
partial_json?: string;
};
index: number;
};
chunkJson = JSON.parse(text);
if (!chunkJson || chunkJson.type === "content_block_stop") {
return finish();
}
const { delta } = chunkJson;
if (delta?.text) {
context.text += delta.text;
options.onUpdate?.(context.text, delta.text);
}
},
onclose() {
finish();
},
onerror(e) {
options.onError?.(e);
throw e;
},
openWhenHidden: true,
});
} catch (e) {
console.error("failed to chat", e);
options.onError?.(e as Error);
}
if (chunkJson?.content_block?.type == "tool_use") {
index += 1;
const id = chunkJson?.content_block.id;
const name = chunkJson?.content_block.name;
runTools.push({
id,
type: "function",
function: {
name,
arguments: "",
},
});
}
if (
chunkJson?.delta?.type == "input_json_delta" &&
chunkJson?.delta?.partial_json
) {
// @ts-ignore
runTools[index]["function"]["arguments"] +=
chunkJson?.delta?.partial_json;
}
return chunkJson?.delta?.text;
},
// processToolMessage, include tool_calls message and tool call results
(
requestPayload: RequestPayload,
toolCallMessage: any,
toolCallResult: any[],
) => {
// reset index value
index = -1;
// @ts-ignore
requestPayload?.messages?.splice(
// @ts-ignore
requestPayload?.messages?.length,
0,
{
role: "assistant",
content: toolCallMessage.tool_calls.map(
(tool: ChatMessageTool) => ({
type: "tool_use",
id: tool.id,
name: tool?.function?.name,
input: tool?.function?.arguments
? JSON.parse(tool?.function?.arguments)
: {},
}),
),
},
// @ts-ignore
...toolCallResult.map((result) => ({
role: "user",
content: [
{
type: "tool_result",
tool_use_id: result.tool_call_id,
content: result.content,
},
],
})),
);
},
options,
);
} else {
const payload = {
method: "POST",
body: JSON.stringify(requestBody),
signal: controller.signal,
headers: {
...getHeaders(), // get common headers
"anthropic-version": accessStore.anthropicApiVersion,
// do not send `anthropicApiKey` in browser!!!
// Authorization: getAuthKey(accessStore.anthropicApiKey),
},
};
try {
controller.signal.onabort = () => options.onFinish("");
controller.signal.onabort = () =>
options.onFinish("", new Response(null, { status: 400 }));
const res = await fetch(path, payload);
const resJson = await res.json();
const message = this.extractMessage(resJson);
options.onFinish(message);
options.onFinish(message, res);
} catch (e) {
console.error("failed to chat", e);
options.onError?.(e as Error);
@@ -370,9 +390,7 @@ export class ClaudeApi implements LLMApi {
if (baseUrl.trim().length === 0) {
const isApp = !!getClientConfig()?.isApp;
baseUrl = isApp
? DEFAULT_API_HOST + "/api/proxy/anthropic"
: ApiPath.Anthropic;
baseUrl = isApp ? ANTHROPIC_BASE_URL : ApiPath.Anthropic;
}
if (!baseUrl.startsWith("http") && !baseUrl.startsWith("/api")) {

View File

@@ -14,6 +14,7 @@ import {
LLMApi,
LLMModel,
MultimodalContent,
SpeechOptions,
} from "../api";
import Locale from "../../locales";
import {
@@ -23,6 +24,7 @@ import {
import { prettyObject } from "@/app/utils/format";
import { getClientConfig } from "@/app/config/client";
import { getMessageTextContent } from "@/app/utils";
import { fetch } from "@/app/utils/stream";
export interface OpenAIListModelResponse {
object: string;
@@ -75,6 +77,10 @@ export class ErnieApi implements LLMApi {
return [baseUrl, path].join("/");
}
speech(options: SpeechOptions): Promise<ArrayBuffer> {
throw new Error("Method not implemented.");
}
async chat(options: ChatOptions) {
const messages = options.messages.map((v) => ({
// "error_code": 336006, "error_msg": "the role of message with even index in the messages must be user or function",
@@ -156,6 +162,7 @@ export class ErnieApi implements LLMApi {
let responseText = "";
let remainText = "";
let finished = false;
let responseRes: Response;
// animate response to make it looks smooth
function animateResponseText() {
@@ -185,19 +192,20 @@ export class ErnieApi implements LLMApi {
const finish = () => {
if (!finished) {
finished = true;
options.onFinish(responseText + remainText);
options.onFinish(responseText + remainText, responseRes);
}
};
controller.signal.onabort = finish;
fetchEventSource(chatPath, {
fetch: fetch as any,
...chatPayload,
async onopen(res) {
clearTimeout(requestTimeoutId);
const contentType = res.headers.get("content-type");
console.log("[Baidu] request response content type: ", contentType);
responseRes = res;
if (contentType?.startsWith("text/plain")) {
responseText = await res.clone().text();
return finish();
@@ -260,7 +268,7 @@ export class ErnieApi implements LLMApi {
const resJson = await res.json();
const message = resJson?.result;
options.onFinish(message);
options.onFinish(message, res);
}
} catch (e) {
console.log("[Request] failed to make a chat request", e);

View File

@@ -13,6 +13,7 @@ import {
LLMApi,
LLMModel,
MultimodalContent,
SpeechOptions,
} from "../api";
import Locale from "../../locales";
import {
@@ -22,6 +23,7 @@ import {
import { prettyObject } from "@/app/utils/format";
import { getClientConfig } from "@/app/config/client";
import { getMessageTextContent } from "@/app/utils";
import { fetch } from "@/app/utils/stream";
export interface OpenAIListModelResponse {
object: string;
@@ -77,6 +79,10 @@ export class DoubaoApi implements LLMApi {
return res.choices?.at(0)?.message?.content ?? "";
}
speech(options: SpeechOptions): Promise<ArrayBuffer> {
throw new Error("Method not implemented.");
}
async chat(options: ChatOptions) {
const messages = options.messages.map((v) => ({
role: v.role,
@@ -124,6 +130,7 @@ export class DoubaoApi implements LLMApi {
let responseText = "";
let remainText = "";
let finished = false;
let responseRes: Response;
// animate response to make it looks smooth
function animateResponseText() {
@@ -153,13 +160,14 @@ export class DoubaoApi implements LLMApi {
const finish = () => {
if (!finished) {
finished = true;
options.onFinish(responseText + remainText);
options.onFinish(responseText + remainText, responseRes);
}
};
controller.signal.onabort = finish;
fetchEventSource(chatPath, {
fetch: fetch as any,
...chatPayload,
async onopen(res) {
clearTimeout(requestTimeoutId);
@@ -168,7 +176,7 @@ export class DoubaoApi implements LLMApi {
"[ByteDance] request response content type: ",
contentType,
);
responseRes = res;
if (contentType?.startsWith("text/plain")) {
responseText = await res.clone().text();
return finish();
@@ -234,7 +242,7 @@ export class DoubaoApi implements LLMApi {
const resJson = await res.json();
const message = this.extractMessage(resJson);
options.onFinish(message);
options.onFinish(message, res);
}
} catch (e) {
console.log("[Request] failed to make a chat request", e);

197
app/client/platforms/glm.ts Normal file
View File

@@ -0,0 +1,197 @@
"use client";
import {
ApiPath,
CHATGLM_BASE_URL,
ChatGLM,
REQUEST_TIMEOUT_MS,
} from "@/app/constant";
import {
useAccessStore,
useAppConfig,
useChatStore,
ChatMessageTool,
usePluginStore,
} from "@/app/store";
import { stream } from "@/app/utils/chat";
import {
ChatOptions,
getHeaders,
LLMApi,
LLMModel,
SpeechOptions,
} from "../api";
import { getClientConfig } from "@/app/config/client";
import { getMessageTextContent } from "@/app/utils";
import { RequestPayload } from "./openai";
import { fetch } from "@/app/utils/stream";
export class ChatGLMApi implements LLMApi {
private disableListModels = true;
path(path: string): string {
const accessStore = useAccessStore.getState();
let baseUrl = "";
if (accessStore.useCustomConfig) {
baseUrl = accessStore.chatglmUrl;
}
if (baseUrl.length === 0) {
const isApp = !!getClientConfig()?.isApp;
const apiPath = ApiPath.ChatGLM;
baseUrl = isApp ? CHATGLM_BASE_URL : apiPath;
}
if (baseUrl.endsWith("/")) {
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
}
if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.ChatGLM)) {
baseUrl = "https://" + baseUrl;
}
console.log("[Proxy Endpoint] ", baseUrl, path);
return [baseUrl, path].join("/");
}
extractMessage(res: any) {
return res.choices?.at(0)?.message?.content ?? "";
}
speech(options: SpeechOptions): Promise<ArrayBuffer> {
throw new Error("Method not implemented.");
}
async chat(options: ChatOptions) {
const 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,
};
console.log("[Request] glm payload: ", requestPayload);
const shouldStream = !!options.config.stream;
const controller = new AbortController();
options.onController?.(controller);
try {
const chatPath = this.path(ChatGLM.ChatPath);
const chatPayload = {
method: "POST",
body: JSON.stringify(requestPayload),
signal: controller.signal,
headers: getHeaders(),
};
// make a fetch request
const requestTimeoutId = setTimeout(
() => controller.abort(),
REQUEST_TIMEOUT_MS,
);
if (shouldStream) {
const [tools, funcs] = usePluginStore
.getState()
.getAsTools(
useChatStore.getState().currentSession().mask?.plugin || [],
);
return stream(
chatPath,
requestPayload,
getHeaders(),
tools as any,
funcs,
controller,
// parseSSE
(text: string, runTools: ChatMessageTool[]) => {
// console.log("parseSSE", text, runTools);
const json = JSON.parse(text);
const choices = json.choices as Array<{
delta: {
content: string;
tool_calls: ChatMessageTool[];
};
}>;
const tool_calls = choices[0]?.delta?.tool_calls;
if (tool_calls?.length > 0) {
const index = tool_calls[0]?.index;
const id = tool_calls[0]?.id;
const args = tool_calls[0]?.function?.arguments;
if (id) {
runTools.push({
id,
type: tool_calls[0]?.type,
function: {
name: tool_calls[0]?.function?.name as string,
arguments: args,
},
});
} else {
// @ts-ignore
runTools[index]["function"]["arguments"] += args;
}
}
return choices[0]?.delta?.content;
},
// processToolMessage, include tool_calls message and tool call results
(
requestPayload: RequestPayload,
toolCallMessage: any,
toolCallResult: any[],
) => {
// @ts-ignore
requestPayload?.messages?.splice(
// @ts-ignore
requestPayload?.messages?.length,
0,
toolCallMessage,
...toolCallResult,
);
},
options,
);
} else {
const res = await fetch(chatPath, chatPayload);
clearTimeout(requestTimeoutId);
const resJson = await res.json();
const message = this.extractMessage(resJson);
options.onFinish(message, res);
}
} catch (e) {
console.log("[Request] failed to make a chat request", e);
options.onError?.(e as Error);
}
}
async usage() {
return {
used: 0,
total: 0,
};
}
async models(): Promise<LLMModel[]> {
return [];
}
}

View File

@@ -1,23 +1,35 @@
import { ApiPath, Google, REQUEST_TIMEOUT_MS } from "@/app/constant";
import { ChatOptions, getHeaders, LLMApi, LLMModel, LLMUsage } from "../api";
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
import { getClientConfig } from "@/app/config/client";
import { DEFAULT_API_HOST } from "@/app/constant";
import Locale from "../../locales";
import {
EventStreamContentType,
fetchEventSource,
} from "@fortaine/fetch-event-source";
import { prettyObject } from "@/app/utils/format";
ChatOptions,
getHeaders,
LLMApi,
LLMModel,
LLMUsage,
SpeechOptions,
} from "../api";
import {
useAccessStore,
useAppConfig,
useChatStore,
usePluginStore,
ChatMessageTool,
} from "@/app/store";
import { stream } from "@/app/utils/chat";
import { getClientConfig } from "@/app/config/client";
import { GEMINI_BASE_URL } from "@/app/constant";
import {
getMessageTextContent,
getMessageImages,
isVisionModel,
} from "@/app/utils";
import { preProcessImageContent } from "@/app/utils/chat";
import { nanoid } from "nanoid";
import { RequestPayload } from "./openai";
import { fetch } from "@/app/utils/stream";
export class GeminiProApi implements LLMApi {
path(path: string): string {
path(path: string, shouldStream = false): string {
const accessStore = useAccessStore.getState();
let baseUrl = "";
@@ -27,7 +39,7 @@ export class GeminiProApi implements LLMApi {
const isApp = !!getClientConfig()?.isApp;
if (baseUrl.length === 0) {
baseUrl = isApp ? DEFAULT_API_HOST + `/api/proxy/google` : ApiPath.Google;
baseUrl = isApp ? GEMINI_BASE_URL : ApiPath.Google;
}
if (baseUrl.endsWith("/")) {
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
@@ -39,12 +51,10 @@ export class GeminiProApi implements LLMApi {
console.log("[Proxy Endpoint] ", baseUrl, path);
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}`;
if (shouldStream) {
chatPath += chatPath.includes("?") ? "&alt=sse" : "?alt=sse";
}
return chatPath;
}
extractMessage(res: any) {
@@ -52,10 +62,15 @@ export class GeminiProApi implements LLMApi {
return (
res?.candidates?.at(0)?.content?.parts.at(0)?.text ||
res?.at(0)?.candidates?.at(0)?.content?.parts.at(0)?.text ||
res?.error?.message ||
""
);
}
speech(options: SpeechOptions): Promise<ArrayBuffer> {
throw new Error("Method not implemented.");
}
async chat(options: ChatOptions): Promise<void> {
const apiClient = this;
let multimodal = false;
@@ -154,7 +169,10 @@ export class GeminiProApi implements LLMApi {
options.onController?.(controller);
try {
// https://github.com/google-gemini/cookbook/blob/main/quickstarts/rest/Streaming_REST.ipynb
const chatPath = this.path(Google.ChatPath(modelConfig.model));
const chatPath = this.path(
Google.ChatPath(modelConfig.model),
shouldStream,
);
const chatPayload = {
method: "POST",
@@ -170,114 +188,84 @@ export class GeminiProApi implements LLMApi {
);
if (shouldStream) {
let responseText = "";
let remainText = "";
let finished = false;
const [tools, funcs] = usePluginStore
.getState()
.getAsTools(
useChatStore.getState().currentSession().mask?.plugin || [],
);
return stream(
chatPath,
requestPayload,
getHeaders(),
// @ts-ignore
tools.length > 0
? // @ts-ignore
[{ functionDeclarations: tools.map((tool) => tool.function) }]
: [],
funcs,
controller,
// parseSSE
(text: string, runTools: ChatMessageTool[]) => {
// console.log("parseSSE", text, runTools);
const chunkJson = JSON.parse(text);
const finish = () => {
if (!finished) {
finished = true;
options.onFinish(responseText + remainText);
}
};
// animate response to make it looks smooth
function animateResponseText() {
if (finished || controller.signal.aborted) {
responseText += remainText;
finish();
return;
}
if (remainText.length > 0) {
const fetchCount = Math.max(1, Math.round(remainText.length / 60));
const fetchText = remainText.slice(0, fetchCount);
responseText += fetchText;
remainText = remainText.slice(fetchCount);
options.onUpdate?.(responseText, fetchText);
}
requestAnimationFrame(animateResponseText);
}
// start animaion
animateResponseText();
controller.signal.onabort = finish;
fetchEventSource(chatPath, {
...chatPayload,
async onopen(res) {
clearTimeout(requestTimeoutId);
const contentType = res.headers.get("content-type");
console.log(
"[Gemini] request response content type: ",
contentType,
const functionCall = chunkJson?.candidates
?.at(0)
?.content.parts.at(0)?.functionCall;
if (functionCall) {
const { name, args } = functionCall;
runTools.push({
id: nanoid(),
type: "function",
function: {
name,
arguments: JSON.stringify(args), // utils.chat call function, using JSON.parse
},
});
}
return chunkJson?.candidates?.at(0)?.content.parts.at(0)?.text;
},
// processToolMessage, include tool_calls message and tool call results
(
requestPayload: RequestPayload,
toolCallMessage: any,
toolCallResult: any[],
) => {
// @ts-ignore
requestPayload?.contents?.splice(
// @ts-ignore
requestPayload?.contents?.length,
0,
{
role: "model",
parts: toolCallMessage.tool_calls.map(
(tool: ChatMessageTool) => ({
functionCall: {
name: tool?.function?.name,
args: JSON.parse(tool?.function?.arguments as string),
},
}),
),
},
// @ts-ignore
...toolCallResult.map((result) => ({
role: "function",
parts: [
{
functionResponse: {
name: result.name,
response: {
name: result.name,
content: result.content, // TODO just text content...
},
},
},
],
})),
);
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 delta = apiClient.extractMessage(json);
if (delta) {
remainText += delta;
}
const blockReason = json?.promptFeedback?.blockReason;
if (blockReason) {
// being blocked
console.log(`[Google] [Safety Ratings] result:`, blockReason);
}
} catch (e) {
console.error("[Request] parse error", text, msg);
}
},
onclose() {
finish();
},
onerror(e) {
options.onError?.(e);
throw e;
},
openWhenHidden: true,
});
options,
);
} else {
const res = await fetch(chatPath, chatPayload);
clearTimeout(requestTimeoutId);
@@ -292,7 +280,7 @@ export class GeminiProApi implements LLMApi {
);
}
const message = apiClient.extractMessage(resJson);
options.onFinish(message);
options.onFinish(message, res);
}
} catch (e) {
console.log("[Request] failed to make a chat request", e);

View File

@@ -1,13 +1,19 @@
"use client";
import {
ApiPath,
DEFAULT_API_HOST,
IFLYTEK_BASE_URL,
Iflytek,
REQUEST_TIMEOUT_MS,
} from "@/app/constant";
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
import { ChatOptions, getHeaders, LLMApi, LLMModel } from "../api";
import {
ChatOptions,
getHeaders,
LLMApi,
LLMModel,
SpeechOptions,
} from "../api";
import Locale from "../../locales";
import {
EventStreamContentType,
@@ -16,8 +22,9 @@ import {
import { prettyObject } from "@/app/utils/format";
import { getClientConfig } from "@/app/config/client";
import { getMessageTextContent } from "@/app/utils";
import { fetch } from "@/app/utils/stream";
import { OpenAIListModelResponse, RequestPayload } from "./openai";
import { RequestPayload } from "./openai";
export class SparkApi implements LLMApi {
private disableListModels = true;
@@ -34,7 +41,7 @@ export class SparkApi implements LLMApi {
if (baseUrl.length === 0) {
const isApp = !!getClientConfig()?.isApp;
const apiPath = ApiPath.Iflytek;
baseUrl = isApp ? DEFAULT_API_HOST + "/proxy" + apiPath : apiPath;
baseUrl = isApp ? IFLYTEK_BASE_URL : apiPath;
}
if (baseUrl.endsWith("/")) {
@@ -53,6 +60,10 @@ export class SparkApi implements LLMApi {
return res.choices?.at(0)?.message?.content ?? "";
}
speech(options: SpeechOptions): Promise<ArrayBuffer> {
throw new Error("Method not implemented.");
}
async chat(options: ChatOptions) {
const messages: ChatOptions["messages"] = [];
for (const v of options.messages) {
@@ -106,6 +117,7 @@ export class SparkApi implements LLMApi {
let responseText = "";
let remainText = "";
let finished = false;
let responseRes: Response;
// Animate response text to make it look smooth
function animateResponseText() {
@@ -132,19 +144,20 @@ export class SparkApi implements LLMApi {
const finish = () => {
if (!finished) {
finished = true;
options.onFinish(responseText + remainText);
options.onFinish(responseText + remainText, responseRes);
}
};
controller.signal.onabort = finish;
fetchEventSource(chatPath, {
fetch: fetch as any,
...chatPayload,
async onopen(res) {
clearTimeout(requestTimeoutId);
const contentType = res.headers.get("content-type");
console.log("[Spark] request response content type: ", contentType);
responseRes = res;
if (contentType?.startsWith("text/plain")) {
responseText = await res.clone().text();
return finish();
@@ -219,7 +232,7 @@ export class SparkApi implements LLMApi {
const resJson = await res.json();
const message = this.extractMessage(resJson);
options.onFinish(message);
options.onFinish(message, res);
}
} catch (e) {
console.log("[Request] failed to make a chat request", e);

View File

@@ -2,35 +2,29 @@
// azure and openai, using same models. so using same LLMApi.
import {
ApiPath,
DEFAULT_API_HOST,
DEFAULT_MODELS,
MOONSHOT_BASE_URL,
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 {
useAccessStore,
useAppConfig,
useChatStore,
ChatMessageTool,
usePluginStore,
} from "@/app/store";
import { stream } from "@/app/utils/chat";
import {
ChatOptions,
getHeaders,
LLMApi,
LLMModel,
LLMUsage,
MultimodalContent,
SpeechOptions,
} from "../api";
import Locale from "../../locales";
import {
EventStreamContentType,
fetchEventSource,
} from "@fortaine/fetch-event-source";
import { prettyObject } from "@/app/utils/format";
import { getClientConfig } from "@/app/config/client";
import { getMessageTextContent } from "@/app/utils";
import { OpenAIListModelResponse, RequestPayload } from "./openai";
import { RequestPayload } from "./openai";
import { fetch } from "@/app/utils/stream";
export class MoonshotApi implements LLMApi {
private disableListModels = true;
@@ -47,7 +41,7 @@ export class MoonshotApi implements LLMApi {
if (baseUrl.length === 0) {
const isApp = !!getClientConfig()?.isApp;
const apiPath = ApiPath.Moonshot;
baseUrl = isApp ? DEFAULT_API_HOST + "/proxy" + apiPath : apiPath;
baseUrl = isApp ? MOONSHOT_BASE_URL : apiPath;
}
if (baseUrl.endsWith("/")) {
@@ -66,6 +60,10 @@ export class MoonshotApi implements LLMApi {
return res.choices?.at(0)?.message?.content ?? "";
}
speech(options: SpeechOptions): Promise<ArrayBuffer> {
throw new Error("Method not implemented.");
}
async chat(options: ChatOptions) {
const messages: ChatOptions["messages"] = [];
for (const v of options.messages) {
@@ -116,122 +114,73 @@ export class MoonshotApi implements LLMApi {
);
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"));
const [tools, funcs] = usePluginStore
.getState()
.getAsTools(
useChatStore.getState().currentSession().mask?.plugin || [],
);
return stream(
chatPath,
requestPayload,
getHeaders(),
tools as any,
funcs,
controller,
// parseSSE
(text: string, runTools: ChatMessageTool[]) => {
// console.log("parseSSE", text, runTools);
const json = JSON.parse(text);
const choices = json.choices as Array<{
delta: {
content: string;
tool_calls: ChatMessageTool[];
};
}>;
const tool_calls = choices[0]?.delta?.tool_calls;
if (tool_calls?.length > 0) {
const index = tool_calls[0]?.index;
const id = tool_calls[0]?.id;
const args = tool_calls[0]?.function?.arguments;
if (id) {
runTools.push({
id,
type: tool_calls[0]?.type,
function: {
name: tool_calls[0]?.function?.name as string,
arguments: args,
},
});
} else {
// @ts-ignore
runTools[index]["function"]["arguments"] += args;
}
}
return;
}
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,
return choices[0]?.delta?.content;
},
// processToolMessage, include tool_calls message and tool call results
(
requestPayload: RequestPayload,
toolCallMessage: any,
toolCallResult: any[],
) => {
// @ts-ignore
requestPayload?.messages?.splice(
// @ts-ignore
requestPayload?.messages?.length,
0,
toolCallMessage,
...toolCallResult,
);
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,
});
options,
);
} else {
const res = await fetch(chatPath, chatPayload);
clearTimeout(requestTimeoutId);
const resJson = await res.json();
const message = this.extractMessage(resJson);
options.onFinish(message);
options.onFinish(message, res);
}
} catch (e) {
console.log("[Request] failed to make a chat request", e);

View File

@@ -2,22 +2,29 @@
// azure and openai, using same models. so using same LLMApi.
import {
ApiPath,
DEFAULT_API_HOST,
OPENAI_BASE_URL,
DEFAULT_MODELS,
OpenaiPath,
Azure,
REQUEST_TIMEOUT_MS,
ServiceProvider,
} from "@/app/constant";
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
import {
ChatMessageTool,
useAccessStore,
useAppConfig,
useChatStore,
usePluginStore,
} from "@/app/store";
import { collectModelsWithDefaultModel } from "@/app/utils/model";
import {
preProcessImageContent,
uploadImage,
base64Image2Blob,
stream,
} from "@/app/utils/chat";
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
import { DalleSize } from "@/app/typing";
import { DalleSize, DalleQuality, DalleStyle } from "@/app/typing";
import {
ChatOptions,
@@ -26,20 +33,16 @@ import {
LLMModel,
LLMUsage,
MultimodalContent,
SpeechOptions,
} from "../api";
import Locale from "../../locales";
import {
EventStreamContentType,
fetchEventSource,
} from "@fortaine/fetch-event-source";
import { prettyObject } from "@/app/utils/format";
import { getClientConfig } from "@/app/config/client";
import {
getMessageTextContent,
getMessageImages,
isVisionModel,
isDalle3 as _isDalle3,
} from "@/app/utils";
import { fetch } from "@/app/utils/stream";
export interface OpenAIListModelResponse {
object: string;
@@ -62,6 +65,7 @@ export interface RequestPayload {
frequency_penalty: number;
top_p: number;
max_tokens?: number;
max_completion_tokens?: number;
}
export interface DalleRequestPayload {
@@ -70,6 +74,8 @@ export interface DalleRequestPayload {
response_format: "url" | "b64_json";
n: number;
size: DalleSize;
quality: DalleQuality;
style: DalleStyle;
}
export class ChatGPTApi implements LLMApi {
@@ -94,7 +100,7 @@ export class ChatGPTApi implements LLMApi {
if (baseUrl.length === 0) {
const isApp = !!getClientConfig()?.isApp;
const apiPath = isAzure ? ApiPath.Azure : ApiPath.OpenAI;
baseUrl = isApp ? DEFAULT_API_HOST + "/proxy" + apiPath : apiPath;
baseUrl = isApp ? OPENAI_BASE_URL : apiPath;
}
if (baseUrl.endsWith("/")) {
@@ -138,6 +144,44 @@ export class ChatGPTApi implements LLMApi {
return res.choices?.at(0)?.message?.content ?? res;
}
async speech(options: SpeechOptions): Promise<ArrayBuffer> {
const requestPayload = {
model: options.model,
input: options.input,
voice: options.voice,
response_format: options.response_format,
speed: options.speed,
};
console.log("[Request] openai speech payload: ", requestPayload);
const controller = new AbortController();
options.onController?.(controller);
try {
const speechPath = this.path(OpenaiPath.SpeechPath);
const speechPayload = {
method: "POST",
body: JSON.stringify(requestPayload),
signal: controller.signal,
headers: getHeaders(),
};
// make a fetch request
const requestTimeoutId = setTimeout(
() => controller.abort(),
REQUEST_TIMEOUT_MS,
);
const res = await fetch(speechPath, speechPayload);
clearTimeout(requestTimeoutId);
return await res.arrayBuffer();
} catch (e) {
console.log("[Request] failed to make a speech request", e);
throw e;
}
}
async chat(options: ChatOptions) {
const modelConfig = {
...useAppConfig.getState().modelConfig,
@@ -151,6 +195,7 @@ export class ChatGPTApi implements LLMApi {
let requestPayload: RequestPayload | DalleRequestPayload;
const isDalle3 = _isDalle3(options.config.model);
const isO1 = options.config.model.startsWith("o1");
if (isDalle3) {
const prompt = getMessageTextContent(
options.messages.slice(-1)?.pop() as any,
@@ -162,6 +207,8 @@ export class ChatGPTApi implements LLMApi {
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);
@@ -170,23 +217,30 @@ export class ChatGPTApi implements LLMApi {
const content = visionModel
? await preProcessImageContent(v.content)
: getMessageTextContent(v);
messages.push({ role: v.role, content });
if (!(isO1 && v.role === "system"))
messages.push({ role: v.role, content });
}
// O1 not support image, tools (plugin in ChatGPTNextWeb) and system, stream, logprobs, temperature, top_p, n, presence_penalty, frequency_penalty yet.
requestPayload = {
messages,
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,
temperature: !isO1 ? modelConfig.temperature : 1,
presence_penalty: !isO1 ? modelConfig.presence_penalty : 0,
frequency_penalty: !isO1 ? modelConfig.frequency_penalty : 0,
top_p: !isO1 ? modelConfig.top_p : 1,
// 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.
};
// O1 使用 max_completion_tokens 控制token数 (https://platform.openai.com/docs/guides/reasoning#controlling-costs)
if (isO1) {
requestPayload["max_completion_tokens"] = modelConfig.max_tokens;
}
// add max_tokens to vision model
if (visionModel && modelConfig.model.includes("preview")) {
if (visionModel) {
requestPayload["max_tokens"] = Math.max(modelConfig.max_tokens, 4000);
}
}
@@ -229,149 +283,91 @@ export class ChatGPTApi implements LLMApi {
isDalle3 ? OpenaiPath.ImagePath : OpenaiPath.ChatPath,
);
}
const chatPayload = {
method: "POST",
body: JSON.stringify(requestPayload),
signal: controller.signal,
headers: getHeaders(),
};
// make a fetch request
const requestTimeoutId = setTimeout(
() => controller.abort(),
isDalle3 ? REQUEST_TIMEOUT_MS * 2 : REQUEST_TIMEOUT_MS, // dalle3 using b64_json is slow.
);
if (shouldStream) {
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"));
let index = -1;
const [tools, funcs] = usePluginStore
.getState()
.getAsTools(
useChatStore.getState().currentSession().mask?.plugin || [],
);
// console.log("getAsTools", tools, funcs);
stream(
chatPath,
requestPayload,
getHeaders(),
tools as any,
funcs,
controller,
// parseSSE
(text: string, runTools: ChatMessageTool[]) => {
// console.log("parseSSE", text, runTools);
const json = JSON.parse(text);
const choices = json.choices as Array<{
delta: {
content: string;
tool_calls: ChatMessageTool[];
};
}>;
const tool_calls = choices[0]?.delta?.tool_calls;
if (tool_calls?.length > 0) {
const id = tool_calls[0]?.id;
const args = tool_calls[0]?.function?.arguments;
if (id) {
index += 1;
runTools.push({
id,
type: tool_calls[0]?.type,
function: {
name: tool_calls[0]?.function?.name as string,
arguments: args,
},
});
} else {
// @ts-ignore
runTools[index]["function"]["arguments"] += args;
}
}
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);
}
return choices[0]?.delta?.content;
},
// processToolMessage, include tool_calls message and tool call results
(
requestPayload: RequestPayload,
toolCallMessage: any,
toolCallResult: any[],
) => {
// reset index value
index = -1;
// @ts-ignore
requestPayload?.messages?.splice(
// @ts-ignore
requestPayload?.messages?.length,
0,
toolCallMessage,
...toolCallResult,
);
},
options,
);
} else {
const chatPayload = {
method: "POST",
body: JSON.stringify(requestPayload),
signal: controller.signal,
headers: getHeaders(),
};
controller.signal.onabort = finish;
// make a fetch request
const requestTimeoutId = setTimeout(
() => controller.abort(),
isDalle3 || isO1 ? REQUEST_TIMEOUT_MS * 4 : REQUEST_TIMEOUT_MS, // dalle3 using b64_json is slow.
);
fetchEventSource(chatPath, {
...chatPayload,
async onopen(res) {
clearTimeout(requestTimeoutId);
const contentType = res.headers.get("content-type");
console.log(
"[OpenAI] request response content type: ",
contentType,
);
if (contentType?.startsWith("text/plain")) {
responseText = await res.clone().text();
return finish();
}
if (
!res.ok ||
!res.headers
.get("content-type")
?.startsWith(EventStreamContentType) ||
res.status !== 200
) {
const responseTexts = [responseText];
let extraInfo = await res.clone().text();
try {
const resJson = await res.clone().json();
extraInfo = prettyObject(resJson);
} catch {}
if (res.status === 401) {
responseTexts.push(Locale.Error.Unauthorized);
}
if (extraInfo) {
responseTexts.push(extraInfo);
}
responseText = responseTexts.join("\n\n");
return finish();
}
},
onmessage(msg) {
if (msg.data === "[DONE]" || finished) {
return finish();
}
const text = msg.data;
try {
const json = JSON.parse(text);
const choices = json.choices as Array<{
delta: { content: string };
}>;
const delta = choices[0]?.delta?.content;
const textmoderation = json?.prompt_filter_results;
if (delta) {
remainText += delta;
}
if (
textmoderation &&
textmoderation.length > 0 &&
ServiceProvider.Azure
) {
const contentFilterResults =
textmoderation[0]?.content_filter_results;
console.log(
`[${ServiceProvider.Azure}] [Text Moderation] flagged categories result:`,
contentFilterResults,
);
}
} catch (e) {
console.error("[Request] parse error", text, msg);
}
},
onclose() {
finish();
},
onerror(e) {
options.onError?.(e);
throw e;
},
openWhenHidden: true,
});
} else {
const res = await fetch(chatPath, chatPayload);
clearTimeout(requestTimeoutId);
const resJson = await res.json();
const message = await this.extractMessage(resJson);
options.onFinish(message);
options.onFinish(message, res);
}
} catch (e) {
console.log("[Request] failed to make a chat request", e);
@@ -457,7 +453,9 @@ export class ChatGPTApi implements LLMApi {
});
const resJson = (await res.json()) as OpenAIListModelResponse;
const chatModels = resJson.data?.filter((m) => m.id.startsWith("gpt-"));
const chatModels = resJson.data?.filter(
(m) => m.id.startsWith("gpt-") || m.id.startsWith("chatgpt-"),
);
console.log("[Models]", chatModels);
if (!chatModels) {

View File

@@ -1,5 +1,5 @@
"use client";
import { ApiPath, DEFAULT_API_HOST, REQUEST_TIMEOUT_MS } from "@/app/constant";
import { ApiPath, TENCENT_BASE_URL, REQUEST_TIMEOUT_MS } from "@/app/constant";
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
import {
@@ -8,6 +8,7 @@ import {
LLMApi,
LLMModel,
MultimodalContent,
SpeechOptions,
} from "../api";
import Locale from "../../locales";
import {
@@ -21,6 +22,7 @@ import mapKeys from "lodash-es/mapKeys";
import mapValues from "lodash-es/mapValues";
import isArray from "lodash-es/isArray";
import isObject from "lodash-es/isObject";
import { fetch } from "@/app/utils/stream";
export interface OpenAIListModelResponse {
object: string;
@@ -69,9 +71,7 @@ export class HunyuanApi implements LLMApi {
if (baseUrl.length === 0) {
const isApp = !!getClientConfig()?.isApp;
baseUrl = isApp
? DEFAULT_API_HOST + "/api/proxy/tencent"
: ApiPath.Tencent;
baseUrl = isApp ? TENCENT_BASE_URL : ApiPath.Tencent;
}
if (baseUrl.endsWith("/")) {
@@ -89,6 +89,10 @@ export class HunyuanApi implements LLMApi {
return res.Choices?.at(0)?.Message?.Content ?? "";
}
speech(options: SpeechOptions): Promise<ArrayBuffer> {
throw new Error("Method not implemented.");
}
async chat(options: ChatOptions) {
const visionModel = isVisionModel(options.config.model);
const messages = options.messages.map((v, index) => ({
@@ -138,6 +142,7 @@ export class HunyuanApi implements LLMApi {
let responseText = "";
let remainText = "";
let finished = false;
let responseRes: Response;
// animate response to make it looks smooth
function animateResponseText() {
@@ -167,13 +172,14 @@ export class HunyuanApi implements LLMApi {
const finish = () => {
if (!finished) {
finished = true;
options.onFinish(responseText + remainText);
options.onFinish(responseText + remainText, responseRes);
}
};
controller.signal.onabort = finish;
fetchEventSource(chatPath, {
fetch: fetch as any,
...chatPayload,
async onopen(res) {
clearTimeout(requestTimeoutId);
@@ -182,7 +188,7 @@ export class HunyuanApi implements LLMApi {
"[Tencent] request response content type: ",
contentType,
);
responseRes = res;
if (contentType?.startsWith("text/plain")) {
responseText = await res.clone().text();
return finish();
@@ -248,7 +254,7 @@ export class HunyuanApi implements LLMApi {
const resJson = await res.json();
const message = this.extractMessage(resJson);
options.onFinish(message);
options.onFinish(message, res);
}
} catch (e) {
console.log("[Request] failed to make a chat request", e);

193
app/client/platforms/xai.ts Normal file
View File

@@ -0,0 +1,193 @@
"use client";
// azure and openai, using same models. so using same LLMApi.
import { ApiPath, XAI_BASE_URL, XAI, REQUEST_TIMEOUT_MS } from "@/app/constant";
import {
useAccessStore,
useAppConfig,
useChatStore,
ChatMessageTool,
usePluginStore,
} from "@/app/store";
import { stream } from "@/app/utils/chat";
import {
ChatOptions,
getHeaders,
LLMApi,
LLMModel,
SpeechOptions,
} from "../api";
import { getClientConfig } from "@/app/config/client";
import { getMessageTextContent } from "@/app/utils";
import { RequestPayload } from "./openai";
import { fetch } from "@/app/utils/stream";
export class XAIApi implements LLMApi {
private disableListModels = true;
path(path: string): string {
const accessStore = useAccessStore.getState();
let baseUrl = "";
if (accessStore.useCustomConfig) {
baseUrl = accessStore.xaiUrl;
}
if (baseUrl.length === 0) {
const isApp = !!getClientConfig()?.isApp;
const apiPath = ApiPath.XAI;
baseUrl = isApp ? XAI_BASE_URL : apiPath;
}
if (baseUrl.endsWith("/")) {
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
}
if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.XAI)) {
baseUrl = "https://" + baseUrl;
}
console.log("[Proxy Endpoint] ", baseUrl, path);
return [baseUrl, path].join("/");
}
extractMessage(res: any) {
return res.choices?.at(0)?.message?.content ?? "";
}
speech(options: SpeechOptions): Promise<ArrayBuffer> {
throw new Error("Method not implemented.");
}
async chat(options: ChatOptions) {
const messages: ChatOptions["messages"] = [];
for (const v of options.messages) {
const content = 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,
};
console.log("[Request] xai payload: ", requestPayload);
const shouldStream = !!options.config.stream;
const controller = new AbortController();
options.onController?.(controller);
try {
const chatPath = this.path(XAI.ChatPath);
const chatPayload = {
method: "POST",
body: JSON.stringify(requestPayload),
signal: controller.signal,
headers: getHeaders(),
};
// make a fetch request
const requestTimeoutId = setTimeout(
() => controller.abort(),
REQUEST_TIMEOUT_MS,
);
if (shouldStream) {
const [tools, funcs] = usePluginStore
.getState()
.getAsTools(
useChatStore.getState().currentSession().mask?.plugin || [],
);
return stream(
chatPath,
requestPayload,
getHeaders(),
tools as any,
funcs,
controller,
// parseSSE
(text: string, runTools: ChatMessageTool[]) => {
// console.log("parseSSE", text, runTools);
const json = JSON.parse(text);
const choices = json.choices as Array<{
delta: {
content: string;
tool_calls: ChatMessageTool[];
};
}>;
const tool_calls = choices[0]?.delta?.tool_calls;
if (tool_calls?.length > 0) {
const index = tool_calls[0]?.index;
const id = tool_calls[0]?.id;
const args = tool_calls[0]?.function?.arguments;
if (id) {
runTools.push({
id,
type: tool_calls[0]?.type,
function: {
name: tool_calls[0]?.function?.name as string,
arguments: args,
},
});
} else {
// @ts-ignore
runTools[index]["function"]["arguments"] += args;
}
}
return choices[0]?.delta?.content;
},
// processToolMessage, include tool_calls message and tool call results
(
requestPayload: RequestPayload,
toolCallMessage: any,
toolCallResult: any[],
) => {
// @ts-ignore
requestPayload?.messages?.splice(
// @ts-ignore
requestPayload?.messages?.length,
0,
toolCallMessage,
...toolCallResult,
);
},
options,
);
} else {
const res = await fetch(chatPath, chatPayload);
clearTimeout(requestTimeoutId);
const resJson = await res.json();
const message = this.extractMessage(resJson);
options.onFinish(message, res);
}
} catch (e) {
console.log("[Request] failed to make a chat request", e);
options.onError?.(e as Error);
}
}
async usage() {
return {
used: 0,
total: 0,
};
}
async models(): Promise<LLMModel[]> {
return [];
}
}

View File

@@ -38,6 +38,7 @@ interface ChatCommands {
next?: Command;
prev?: Command;
clear?: Command;
fork?: Command;
del?: Command;
}

View File

@@ -1,6 +1,12 @@
import { useEffect, useState, useRef, useMemo } from "react";
import {
useEffect,
useState,
useRef,
useMemo,
forwardRef,
useImperativeHandle,
} from "react";
import { useParams } from "react-router";
import { useWindowSize } from "@/app/utils";
import { IconButton } from "./button";
import { nanoid } from "nanoid";
import ExportIcon from "../icons/share.svg";
@@ -8,6 +14,7 @@ import CopyIcon from "../icons/copy.svg";
import DownloadIcon from "../icons/download.svg";
import GithubIcon from "../icons/github.svg";
import LoadingButtonIcon from "../icons/loading.svg";
import ReloadButtonIcon from "../icons/reload.svg";
import Locale from "../locales";
import { Modal, showToast } from "./ui-lib";
import { copyToClipboard, downloadAs } from "../utils";
@@ -15,73 +22,89 @@ import { Path, ApiPath, REPO_URL } from "@/app/constant";
import { Loading } from "./home";
import styles from "./artifacts.module.scss";
export function HTMLPreview(props: {
type HTMLPreviewProps = {
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);
export type HTMLPreviewHander = {
reload: () => void;
};
export const HTMLPreview = forwardRef<HTMLPreviewHander, HTMLPreviewProps>(
function HTMLPreview(props, ref) {
const iframeRef = useRef<HTMLIFrameElement>(null);
const [frameId, setFrameId] = useState<string>(nanoid());
const [iframeHeight, setIframeHeight] = useState(600);
const [title, setTitle] = useState("");
/*
* https://stackoverflow.com/questions/19739001/what-is-the-difference-between-srcdoc-and-src-datatext-html-in-an
* 1. using srcdoc
* 2. using src with dataurl:
* easy to share
* length limit (Data URIs cannot be larger than 32,768 characters.)
*/
useEffect(() => {
const handleMessage = (e: any) => {
const { id, height, title } = e.data;
setTitle(title);
if (id == frameId) {
setIframeHeight(height);
}
};
window.addEventListener("message", handleMessage);
return () => {
window.removeEventListener("message", handleMessage);
};
}, [frameId]);
useImperativeHandle(ref, () => ({
reload: () => {
setFrameId(nanoid());
},
}));
const height = useMemo(() => {
if (!props.autoHeight) return props.height || 600;
if (typeof props.height === "string") {
return props.height;
}
const parentHeight = props.height || 600;
return iframeHeight + 40 > parentHeight
? parentHeight
: iframeHeight + 40;
}, [props.autoHeight, props.height, iframeHeight]);
const srcDoc = useMemo(() => {
const script = `<script>window.addEventListener("DOMContentLoaded", () => new ResizeObserver((entries) => parent.postMessage({id: '${frameId}', height: entries[0].target.clientHeight}, '*')).observe(document.body))</script>`;
if (props.code.includes("<!DOCTYPE html>")) {
props.code.replace("<!DOCTYPE html>", "<!DOCTYPE html>" + script);
}
return script + props.code;
}, [props.code, frameId]);
const handleOnLoad = () => {
if (props?.onLoad) {
props.onLoad(title);
}
};
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}
/>
);
}
return (
<iframe
className={styles["artifacts-iframe"]}
key={frameId}
ref={iframeRef}
sandbox="allow-forms allow-modals allow-scripts"
style={{ height }}
srcDoc={srcDoc}
onLoad={handleOnLoad}
/>
);
},
);
export function ArtifactsShareButton({
getCode,
@@ -184,6 +207,7 @@ export function Artifacts() {
const [code, setCode] = useState("");
const [loading, setLoading] = useState(true);
const [fileName, setFileName] = useState("");
const previewRef = useRef<HTMLPreviewHander>(null);
useEffect(() => {
if (id) {
@@ -208,6 +232,13 @@ export function Artifacts() {
<a href={REPO_URL} target="_blank" rel="noopener noreferrer">
<IconButton bordered icon={<GithubIcon />} shadow />
</a>
<IconButton
bordered
style={{ marginLeft: 20 }}
icon={<ReloadButtonIcon />}
shadow
onClick={() => previewRef.current?.reload()}
/>
<div className={styles["artifacts-title"]}>NextChat Artifacts</div>
<ArtifactsShareButton
id={id}
@@ -220,6 +251,7 @@ export function Artifacts() {
{code && (
<HTMLPreview
code={code}
ref={previewRef}
autoHeight={false}
height={"100%"}
onLoad={(title) => {

View File

@@ -1,12 +1,70 @@
.auth-page {
display: flex;
justify-content: center;
justify-content: flex-start;
align-items: center;
height: 100%;
width: 100%;
flex-direction: column;
.top-banner {
position: relative;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
padding: 12px 64px;
box-sizing: border-box;
background: var(--second);
.top-banner-inner {
display: flex;
justify-content: center;
align-items: center;
font-size: 14px;
line-height: 150%;
span {
gap: 8px;
a {
display: inline-flex;
align-items: center;
text-decoration: none;
margin-left: 8px;
color: var(--primary);
}
}
}
.top-banner-close {
cursor: pointer;
position: absolute;
top: 50%;
right: 48px;
transform: translateY(-50%);
}
}
@media (max-width: 600px) {
.top-banner {
padding: 12px 24px 12px 12px;
.top-banner-close {
right: 10px;
}
.top-banner-inner {
.top-banner-logo {
margin-right: 8px;
}
}
}
}
.auth-header {
display: flex;
justify-content: space-between;
width: 100%;
padding: 10px;
box-sizing: border-box;
animation: slide-in-from-top ease 0.3s;
}
.auth-logo {
margin-top: 10vh;
transform: scale(1.4);
}
@@ -14,6 +72,7 @@
font-size: 24px;
font-weight: bold;
line-height: 2;
margin-bottom: 1vh;
}
.auth-tips {
@@ -24,6 +83,10 @@
margin: 3vh 0;
}
.auth-input-second {
margin: 0 0 3vh 0;
}
.auth-actions {
display: flex;
justify-content: center;

View File

@@ -1,21 +1,37 @@
import styles from "./auth.module.scss";
import { IconButton } from "./button";
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { Path } from "../constant";
import { Path, SAAS_CHAT_URL } from "../constant";
import { useAccessStore } from "../store";
import Locale from "../locales";
import Delete from "../icons/close.svg";
import Arrow from "../icons/arrow.svg";
import Logo from "../icons/logo.svg";
import { useMobileScreen } from "@/app/utils";
import BotIcon from "../icons/bot.svg";
import { useEffect } from "react";
import { getClientConfig } from "../config/client";
import { PasswordInput } from "./ui-lib";
import LeftIcon from "@/app/icons/left.svg";
import { safeLocalStorage } from "@/app/utils";
import {
trackSettingsPageGuideToCPaymentClick,
trackAuthorizationPageButtonToCPaymentClick,
} from "../utils/auth-settings-events";
import clsx from "clsx";
const storage = safeLocalStorage();
export function AuthPage() {
const navigate = useNavigate();
const accessStore = useAccessStore();
const goHome = () => navigate(Path.Home);
const goChat = () => navigate(Path.Chat);
const goSaas = () => {
trackAuthorizationPageButtonToCPaymentClick();
window.location.href = SAAS_CHAT_URL;
};
const resetAccessCode = () => {
accessStore.update((access) => {
access.openaiApiKey = "";
@@ -32,43 +48,58 @@ export function AuthPage() {
return (
<div className={styles["auth-page"]}>
<div className={`no-dark ${styles["auth-logo"]}`}>
<TopBanner></TopBanner>
<div className={styles["auth-header"]}>
<IconButton
icon={<LeftIcon />}
text={Locale.Auth.Return}
onClick={() => navigate(Path.Home)}
></IconButton>
</div>
<div className={clsx("no-dark", styles["auth-logo"])}>
<BotIcon />
</div>
<div className={styles["auth-title"]}>{Locale.Auth.Title}</div>
<div className={styles["auth-tips"]}>{Locale.Auth.Tips}</div>
<input
className={styles["auth-input"]}
type="password"
placeholder={Locale.Auth.Input}
<PasswordInput
style={{ marginTop: "3vh", marginBottom: "3vh" }}
aria={Locale.Settings.ShowPassword}
aria-label={Locale.Auth.Input}
value={accessStore.accessCode}
type="text"
placeholder={Locale.Auth.Input}
onChange={(e) => {
accessStore.update(
(access) => (access.accessCode = e.currentTarget.value),
);
}}
/>
{!accessStore.hideUserApiKey ? (
<>
<div className={styles["auth-tips"]}>{Locale.Auth.SubTips}</div>
<input
className={styles["auth-input"]}
type="password"
placeholder={Locale.Settings.Access.OpenAI.ApiKey.Placeholder}
<PasswordInput
style={{ marginTop: "3vh", marginBottom: "3vh" }}
aria={Locale.Settings.ShowPassword}
aria-label={Locale.Settings.Access.OpenAI.ApiKey.Placeholder}
value={accessStore.openaiApiKey}
type="text"
placeholder={Locale.Settings.Access.OpenAI.ApiKey.Placeholder}
onChange={(e) => {
accessStore.update(
(access) => (access.openaiApiKey = e.currentTarget.value),
);
}}
/>
<input
className={styles["auth-input"]}
type="password"
placeholder={Locale.Settings.Access.Google.ApiKey.Placeholder}
<PasswordInput
style={{ marginTop: "3vh", marginBottom: "3vh" }}
aria={Locale.Settings.ShowPassword}
aria-label={Locale.Settings.Access.Google.ApiKey.Placeholder}
value={accessStore.googleApiKey}
type="text"
placeholder={Locale.Settings.Access.Google.ApiKey.Placeholder}
onChange={(e) => {
accessStore.update(
(access) => (access.googleApiKey = e.currentTarget.value),
@@ -85,13 +116,74 @@ export function AuthPage() {
onClick={goChat}
/>
<IconButton
text={Locale.Auth.Later}
text={Locale.Auth.SaasTips}
onClick={() => {
resetAccessCode();
goHome();
goSaas();
}}
/>
</div>
</div>
);
}
function TopBanner() {
const [isHovered, setIsHovered] = useState(false);
const [isVisible, setIsVisible] = useState(true);
const isMobile = useMobileScreen();
useEffect(() => {
// 检查 localStorage 中是否有标记
const bannerDismissed = storage.getItem("bannerDismissed");
// 如果标记不存在,存储默认值并显示横幅
if (!bannerDismissed) {
storage.setItem("bannerDismissed", "false");
setIsVisible(true); // 显示横幅
} else if (bannerDismissed === "true") {
// 如果标记为 "true",则隐藏横幅
setIsVisible(false);
}
}, []);
const handleMouseEnter = () => {
setIsHovered(true);
};
const handleMouseLeave = () => {
setIsHovered(false);
};
const handleClose = () => {
setIsVisible(false);
storage.setItem("bannerDismissed", "true");
};
if (!isVisible) {
return null;
}
return (
<div
className={styles["top-banner"]}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<div className={clsx(styles["top-banner-inner"], "no-dark")}>
<Logo className={styles["top-banner-logo"]}></Logo>
<span>
{Locale.Auth.TopTips}
<a
href={SAAS_CHAT_URL}
rel="stylesheet"
onClick={() => {
trackSettingsPageGuideToCPaymentClick();
}}
>
{Locale.Settings.Access.SaasStart.ChatNow}
<Arrow style={{ marginLeft: "4px" }} />
</a>
</span>
</div>
{(isHovered || isMobile) && (
<Delete className={styles["top-banner-close"]} onClick={handleClose} />
)}
</div>
);
}

View File

@@ -5,7 +5,6 @@
align-items: center;
justify-content: center;
padding: 10px;
cursor: pointer;
transition: all 0.3s ease;
overflow: hidden;

View File

@@ -2,6 +2,7 @@ import * as React from "react";
import styles from "./button.module.scss";
import { CSSProperties } from "react";
import clsx from "clsx";
export type ButtonType = "primary" | "danger" | null;
@@ -18,15 +19,20 @@ export function IconButton(props: {
tabIndex?: number;
autoFocus?: boolean;
style?: CSSProperties;
aria?: string;
}) {
return (
<button
className={
styles["icon-button"] +
` ${props.bordered && styles.border} ${props.shadow && styles.shadow} ${
props.className ?? ""
} clickable ${styles[props.type ?? ""]}`
}
className={clsx(
"clickable",
styles["icon-button"],
{
[styles.border]: props.bordered,
[styles.shadow]: props.shadow,
},
styles[props.type ?? ""],
props.className,
)}
onClick={props.onClick}
title={props.title}
disabled={props.disabled}
@@ -34,20 +40,26 @@ export function IconButton(props: {
tabIndex={props.tabIndex}
autoFocus={props.autoFocus}
style={props.style}
aria-label={props.aria}
>
{props.icon && (
<div
className={
styles["icon-button-icon"] +
` ${props.type === "primary" && "no-dark"}`
}
aria-label={props.text || props.title}
className={clsx(styles["icon-button-icon"], {
"no-dark": props.type === "primary",
})}
>
{props.icon}
</div>
)}
{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>
);

View File

@@ -1,5 +1,4 @@
import DeleteIcon from "../icons/delete.svg";
import BotIcon from "../icons/bot.svg";
import styles from "./home.module.scss";
import {
@@ -12,13 +11,14 @@ import {
import { useChatStore } from "../store";
import Locale from "../locales";
import { Link, useLocation, useNavigate } from "react-router-dom";
import { useLocation, useNavigate } from "react-router-dom";
import { Path } from "../constant";
import { MaskAvatar } from "./mask";
import { Mask } from "../store/mask";
import { useRef, useEffect } from "react";
import { showConfirm } from "./ui-lib";
import { useMobileScreen } from "../utils";
import clsx from "clsx";
export function ChatItem(props: {
onClick?: () => void;
@@ -46,11 +46,11 @@ export function ChatItem(props: {
<Draggable draggableId={`${props.id}`} index={props.index}>
{(provided) => (
<div
className={`${styles["chat-item"]} ${
props.selected &&
(currentPath === Path.Chat || currentPath === Path.Home) &&
styles["chat-item-selected"]
}`}
className={clsx(styles["chat-item"], {
[styles["chat-item-selected"]]:
props.selected &&
(currentPath === Path.Chat || currentPath === Path.Home),
})}
onClick={props.onClick}
ref={(ele) => {
draggableRef.current = ele;
@@ -64,7 +64,7 @@ export function ChatItem(props: {
>
{props.narrow ? (
<div className={styles["chat-item-narrow"]}>
<div className={styles["chat-item-avatar"] + " no-dark"}>
<div className={clsx(styles["chat-item-avatar"], "no-dark")}>
<MaskAvatar
avatar={props.mask.avatar}
model={props.mask.modelConfig.model}

View File

@@ -45,6 +45,14 @@
.chat-input-actions {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 5px;
&-end {
display: flex;
margin-left: auto;
gap: 5px;
}
.chat-input-action {
display: inline-flex;
@@ -62,10 +70,6 @@
width: var(--icon-width);
overflow: hidden;
&:not(:last-child) {
margin-right: 5px;
}
.text {
white-space: nowrap;
padding-left: 5px;
@@ -231,10 +235,12 @@
animation: slide-in ease 0.3s;
$linear: linear-gradient(to right,
rgba(0, 0, 0, 0),
rgba(0, 0, 0, 1),
rgba(0, 0, 0, 0));
$linear: linear-gradient(
to right,
rgba(0, 0, 0, 0),
rgba(0, 0, 0, 1),
rgba(0, 0, 0, 0)
);
mask-image: $linear;
@mixin show {
@@ -346,6 +352,12 @@
flex-wrap: nowrap;
}
}
.chat-model-name {
font-size: 12px;
color: var(--black);
margin-left: 6px;
}
}
.chat-message-container {
@@ -367,7 +379,7 @@
}
}
.chat-message-user>.chat-message-container {
.chat-message-user > .chat-message-container {
align-items: flex-end;
}
@@ -407,6 +419,21 @@
margin-top: 5px;
}
.chat-message-tools {
font-size: 12px;
color: #aaa;
line-height: 1.5;
margin-top: 5px;
.chat-message-tool {
display: flex;
align-items: end;
svg {
margin-left: 5px;
margin-right: 5px;
}
}
}
.chat-message-item {
box-sizing: border-box;
max-width: 100%;
@@ -422,6 +449,25 @@
transition: all ease 0.3s;
}
.chat-message-audio {
display: flex;
align-items: center;
justify-content: space-between;
border-radius: 10px;
background-color: rgba(0, 0, 0, 0.05);
border: var(--border-in-light);
position: relative;
transition: all ease 0.3s;
margin-top: 10px;
font-size: 14px;
user-select: text;
word-break: break-word;
box-sizing: border-box;
audio {
height: 30px; /* 调整高度 */
}
}
.chat-message-item-image {
width: 100%;
margin-top: 10px;
@@ -450,23 +496,27 @@
border: rgba($color: #888, $alpha: 0.2) 1px solid;
}
@media only screen and (max-width: 600px) {
$calc-image-width: calc(100vw/3*2/var(--image-count));
$calc-image-width: calc(100vw / 3 * 2 / var(--image-count));
.chat-message-item-image-multi {
width: $calc-image-width;
height: $calc-image-width;
}
.chat-message-item-image {
max-width: calc(100vw/3*2);
max-width: calc(100vw / 3 * 2);
}
}
@media screen and (min-width: 600px) {
$max-image-width: calc(calc(1200px - var(--sidebar-width))/3*2/var(--image-count));
$image-width: calc(calc(var(--window-width) - var(--sidebar-width))/3*2/var(--image-count));
$max-image-width: calc(
calc(1200px - var(--sidebar-width)) / 3 * 2 / var(--image-count)
);
$image-width: calc(
calc(var(--window-width) - var(--sidebar-width)) / 3 * 2 /
var(--image-count)
);
.chat-message-item-image-multi {
width: $image-width;
@@ -476,7 +526,7 @@
}
.chat-message-item-image {
max-width: calc(calc(1200px - var(--sidebar-width))/3*2);
max-width: calc(calc(1200px - var(--sidebar-width)) / 3 * 2);
}
}
@@ -494,7 +544,7 @@
z-index: 1;
}
.chat-message-user>.chat-message-container>.chat-message-item {
.chat-message-user > .chat-message-container > .chat-message-item {
background-color: var(--second);
&:hover {
@@ -605,7 +655,8 @@
min-height: 68px;
}
.chat-input:focus {}
.chat-input:focus {
}
.chat-input-send {
background-color: var(--primary);
@@ -624,4 +675,79 @@
.chat-input-send {
bottom: 30px;
}
}
}
.shortcut-key-container {
padding: 10px;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.shortcut-key-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 16px;
}
.shortcut-key-item {
display: flex;
justify-content: space-between;
align-items: center;
overflow: hidden;
padding: 10px;
background-color: var(--white);
}
.shortcut-key-title {
font-size: 14px;
color: var(--black);
}
.shortcut-key-keys {
display: flex;
gap: 8px;
}
.shortcut-key {
display: flex;
align-items: center;
justify-content: center;
border: var(--border-in-light);
border-radius: 8px;
padding: 4px;
background-color: var(--gray);
min-width: 32px;
}
.shortcut-key span {
font-size: 12px;
color: var(--black);
}
.chat-main {
display: flex;
height: 100%;
width: 100%;
position: relative;
overflow: hidden;
.chat-body-container {
height: 100%;
display: flex;
flex-direction: column;
flex: 1;
width: 100%;
}
.chat-side-panel {
position: absolute;
inset: 0;
background: var(--white);
overflow: hidden;
z-index: 10;
transform: translateX(100%);
transition: all ease 0.3s;
&-show {
transform: translateX(0);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -36,7 +36,9 @@ export function Avatar(props: { model?: ModelType; avatar?: string }) {
if (props.model) {
return (
<div className="no-dark">
{props.model?.startsWith("gpt-4") ? (
{props.model?.startsWith("gpt-4") ||
props.model?.startsWith("chatgpt-4o") ||
props.model?.startsWith("o1") ? (
<BlackBotIcon className="user-avatar" />
) : (
<BotIcon className="user-avatar" />

View File

@@ -8,6 +8,7 @@ import { ISSUE_URL } from "../constant";
import Locale from "../locales";
import { showConfirm } from "./ui-lib";
import { useSyncStore } from "../store/sync";
import { useChatStore } from "../store/chat";
interface IErrorBoundaryState {
hasError: boolean;
@@ -30,8 +31,7 @@ export class ErrorBoundary extends React.Component<any, IErrorBoundaryState> {
try {
useSyncStore.getState().export();
} finally {
localStorage.clear();
location.reload();
useChatStore.getState().clearAllData();
}
}

View File

@@ -1,5 +1,5 @@
/* eslint-disable @next/next/no-img-element */
import { ChatMessage, ModelType, useAppConfig, useChatStore } from "../store";
import { ChatMessage, useAppConfig, useChatStore } from "../store";
import Locale from "../locales";
import styles from "./exporter.module.scss";
import {
@@ -40,6 +40,7 @@ import { EXPORT_MESSAGE_CLASS_NAME } from "../constant";
import { getClientConfig } from "../config/client";
import { type ClientApi, getClientApi } from "../client/api";
import { getMessageTextContent } from "../utils";
import clsx from "clsx";
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
loading: () => <LoadingIcon />,
@@ -118,9 +119,10 @@ function Steps<
return (
<div
key={i}
className={`${styles["step"]} ${
styles[i <= props.index ? "step-finished" : ""]
} ${i === props.index && styles["step-current"]} clickable`}
className={clsx("clickable", styles["step"], {
[styles["step-finished"]]: i <= props.index,
[styles["step-current"]]: i === props.index,
})}
onClick={() => {
props.onStepChange?.(i);
}}
@@ -525,11 +527,11 @@ export function ImagePreviewer(props: {
messages={props.messages}
/>
<div
className={`${styles["preview-body"]} ${styles["default-theme"]}`}
className={clsx(styles["preview-body"], styles["default-theme"])}
ref={previewRef}
>
<div className={styles["chat-info"]}>
<div className={styles["logo"] + " no-dark"}>
<div className={clsx(styles["logo"], "no-dark")}>
<NextImage
src={ChatGptIcon.src}
alt="logo"
@@ -570,7 +572,7 @@ export function ImagePreviewer(props: {
{props.messages.map((m, i) => {
return (
<div
className={styles["message"] + " " + styles["message-" + m.role]}
className={clsx(styles["message"], styles["message-" + m.role])}
key={i}
>
<div className={styles["avatar"]}>

View File

@@ -140,6 +140,9 @@
display: flex;
justify-content: space-between;
align-items: center;
&-narrow {
justify-content: center;
}
}
.sidebar-logo {

View File

@@ -3,7 +3,6 @@
require("../polyfill");
import { useState, useEffect } from "react";
import styles from "./home.module.scss";
import BotIcon from "../icons/bot.svg";
@@ -29,10 +28,11 @@ import { AuthPage } from "./auth";
import { getClientConfig } from "../config/client";
import { type ClientApi, getClientApi } from "../client/api";
import { useAccessStore } from "../store";
import clsx from "clsx";
export function Loading(props: { noLogo?: boolean }) {
return (
<div className={styles["loading-content"] + " no-dark"}>
<div className={clsx("no-dark", styles["loading-content"])}>
{!props.noLogo && <BotIcon />}
<LoadingIcon />
</div>
@@ -59,6 +59,17 @@ const MaskPage = dynamic(async () => (await import("./mask")).MaskPage, {
loading: () => <Loading noLogo />,
});
const PluginPage = dynamic(async () => (await import("./plugin")).PluginPage, {
loading: () => <Loading noLogo />,
});
const SearchChat = dynamic(
async () => (await import("./search-chat")).SearchChatPage,
{
loading: () => <Loading noLogo />,
},
);
const Sd = dynamic(async () => (await import("./sd")).Sd, {
loading: () => <Loading noLogo />,
});
@@ -168,12 +179,18 @@ function Screen() {
if (isSdNew) return <Sd />;
return (
<>
<SideBar className={isHome ? styles["sidebar-show"] : ""} />
<SideBar
className={clsx({
[styles["sidebar-show"]]: isHome,
})}
/>
<WindowContent>
<Routes>
<Route path={Path.Home} element={<Chat />} />
<Route path={Path.NewChat} element={<NewChat />} />
<Route path={Path.Masks} element={<MaskPage />} />
<Route path={Path.Plugins} element={<PluginPage />} />
<Route path={Path.SearchChat} element={<SearchChat />} />
<Route path={Path.Chat} element={<Chat />} />
<Route path={Path.Settings} element={<Settings />} />
</Routes>
@@ -184,9 +201,10 @@ function Screen() {
return (
<div
className={`${styles.container} ${
shouldTightBorder ? styles["tight-container"] : styles.container
} ${getLang() === "ar" ? styles["rtl-screen"] : ""}`}
className={clsx(styles.container, {
[styles["tight-container"]]: shouldTightBorder,
[styles["rtl-screen"]]: getLang() === "ar",
})}
>
{renderContent()}
</div>

View File

@@ -1,5 +1,6 @@
import * as React from "react";
import styles from "./input-range.module.scss";
import clsx from "clsx";
interface InputRangeProps {
onChange: React.ChangeEventHandler<HTMLInputElement>;
@@ -9,6 +10,7 @@ interface InputRangeProps {
min: string;
max: string;
step: string;
aria: string;
}
export function InputRange({
@@ -19,11 +21,13 @@ export function InputRange({
min,
max,
step,
aria,
}: InputRangeProps) {
return (
<div className={styles["input-range"] + ` ${className ?? ""}`}>
<div className={clsx(styles["input-range"], className)}>
{title || value}
<input
aria-label={aria}
type="range"
title={title}
value={value}

View File

@@ -8,14 +8,23 @@ import RehypeHighlight from "rehype-highlight";
import { useRef, useState, RefObject, useEffect, useMemo } from "react";
import { copyToClipboard, useWindowSize } from "../utils";
import mermaid from "mermaid";
import Locale from "../locales";
import LoadingIcon from "../icons/three-dots.svg";
import ReloadButtonIcon from "../icons/reload.svg";
import React from "react";
import { useDebouncedCallback } from "use-debounce";
import { showImageModal, FullScreen } from "./ui-lib";
import { ArtifactsShareButton, HTMLPreview } from "./artifacts";
import { Plugin } from "../constant";
import {
ArtifactsShareButton,
HTMLPreview,
HTMLPreviewHander,
} from "./artifacts";
import { useChatStore } from "../store";
import { IconButton } from "./button";
import { useAppConfig } from "../store/config";
import clsx from "clsx";
export function Mermaid(props: { code: string }) {
const ref = useRef<HTMLDivElement>(null);
const [hasError, setHasError] = useState(false);
@@ -49,7 +58,7 @@ export function Mermaid(props: { code: string }) {
return (
<div
className="no-dark mermaid"
className={clsx("no-dark", "mermaid")}
style={{
cursor: "pointer",
overflow: "auto",
@@ -64,13 +73,12 @@ export function Mermaid(props: { code: string }) {
export function PreCode(props: { children: any }) {
const ref = useRef<HTMLPreElement>(null);
const refText = ref.current?.innerText;
const previewRef = useRef<HTMLPreviewHander>(null);
const [mermaidCode, setMermaidCode] = useState("");
const [htmlCode, setHtmlCode] = useState("");
const { height } = useWindowSize();
const chatStore = useChatStore();
const session = chatStore.currentSession();
const plugins = session.mask?.plugin;
const renderArtifacts = useDebouncedCallback(() => {
if (!ref.current) return;
@@ -79,22 +87,21 @@ export function PreCode(props: { children: any }) {
setMermaidCode((mermaidDom as HTMLElement).innerText);
}
const htmlDom = ref.current.querySelector("code.language-html");
const refText = ref.current.querySelector("code")?.innerText;
if (htmlDom) {
setHtmlCode((htmlDom as HTMLElement).innerText);
} else if (refText?.startsWith("<!DOCTYPE")) {
} else if (
refText?.startsWith("<!DOCTYPE") ||
refText?.startsWith("<svg") ||
refText?.startsWith("<?xml")
) {
setHtmlCode(refText);
}
}, 600);
useEffect(() => {
setTimeout(renderArtifacts, 1);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [refText]);
const enableArtifacts = useMemo(
() => plugins?.includes(Plugin.Artifacts),
[plugins],
);
const config = useAppConfig();
const enableArtifacts =
session.mask?.enableArtifacts !== false && config.enableArtifacts;
//Wrap the paragraph for plain-text
useEffect(() => {
@@ -119,6 +126,7 @@ export function PreCode(props: { children: any }) {
codeElement.style.whiteSpace = "pre-wrap";
}
});
setTimeout(renderArtifacts, 1);
}
}, []);
@@ -129,8 +137,9 @@ export function PreCode(props: { children: any }) {
className="copy-code-button"
onClick={() => {
if (ref.current) {
const code = ref.current.innerText;
copyToClipboard(code);
copyToClipboard(
ref.current.querySelector("code")?.innerText ?? "",
);
}
}}
></span>
@@ -145,7 +154,15 @@ export function PreCode(props: { children: any }) {
style={{ position: "absolute", right: 20, top: 10 }}
getCode={() => htmlCode}
/>
<IconButton
style={{ position: "absolute", right: 120, top: 10 }}
bordered
icon={<ReloadButtonIcon />}
shadow
onClick={() => previewRef.current?.reload()}
/>
<HTMLPreview
ref={previewRef}
code={htmlCode}
autoHeight={!document.fullscreenElement}
height={!document.fullscreenElement ? 600 : height}
@@ -156,21 +173,59 @@ export function PreCode(props: { children: any }) {
);
}
function escapeDollarNumber(text: string) {
let escapedText = "";
function CustomCode(props: { children: any; className?: string }) {
const chatStore = useChatStore();
const session = chatStore.currentSession();
const config = useAppConfig();
const enableCodeFold =
session.mask?.enableCodeFold !== false && config.enableCodeFold;
for (let i = 0; i < text.length; i += 1) {
let char = text[i];
const nextChar = text[i + 1] || " ";
const ref = useRef<HTMLPreElement>(null);
const [collapsed, setCollapsed] = useState(true);
const [showToggle, setShowToggle] = useState(false);
if (char === "$" && nextChar >= "0" && nextChar <= "9") {
char = "\\$";
useEffect(() => {
if (ref.current) {
const codeHeight = ref.current.scrollHeight;
setShowToggle(codeHeight > 400);
ref.current.scrollTop = ref.current.scrollHeight;
}
}, [props.children]);
escapedText += char;
}
const toggleCollapsed = () => {
setCollapsed((collapsed) => !collapsed);
};
const renderShowMoreButton = () => {
if (showToggle && enableCodeFold && collapsed) {
return (
<div
className={clsx("show-hide-button", {
collapsed,
expanded: !collapsed,
})}
>
<button onClick={toggleCollapsed}>{Locale.NewChat.More}</button>
</div>
);
}
return null;
};
return (
<>
<code
className={clsx(props?.className)}
ref={ref}
style={{
maxHeight: enableCodeFold && collapsed ? "400px" : "none",
overflowY: "hidden",
}}
>
{props.children}
</code>
return escapedText;
{renderShowMoreButton()}
</>
);
}
function escapeBrackets(text: string) {
@@ -191,9 +246,30 @@ function escapeBrackets(text: string) {
);
}
function tryWrapHtmlCode(text: string) {
// try add wrap html code (fixed: html codeblock include 2 newline)
// ignore embed codeblock
if (text.includes("```")) {
return text;
}
return text
.replace(
/([`]*?)(\w*?)([\n\r]*?)(<!DOCTYPE html>)/g,
(match, quoteStart, lang, newLine, doctype) => {
return !quoteStart ? "\n```html\n" + doctype : match;
},
)
.replace(
/(<\/body>)([\r\n\s]*?)(<\/html>)([\n\r]*)([`]*)([\n\r]*?)/g,
(match, bodyEnd, space, htmlEnd, newLine, quoteEnd) => {
return !quoteEnd ? bodyEnd + space + htmlEnd + "\n```\n" : match;
},
);
}
function _MarkDownContent(props: { content: string }) {
const escapedContent = useMemo(() => {
return escapeBrackets(escapeDollarNumber(props.content));
return tryWrapHtmlCode(escapeBrackets(props.content));
}, [props.content]);
return (
@@ -211,9 +287,24 @@ function _MarkDownContent(props: { content: string }) {
]}
components={{
pre: PreCode,
code: CustomCode,
p: (pProps) => <p {...pProps} dir="auto" />,
a: (aProps) => {
const href = aProps.href || "";
if (/\.(aac|mp3|opus|wav)$/.test(href)) {
return (
<figure>
<audio controls src={href}></audio>
</figure>
);
}
if (/\.(3gp|3g2|webm|ogv|mpeg|mp4|avi)$/.test(href)) {
return (
<video controls width="99.9%">
<source src={href} />
</video>
);
}
const isInternal = /^\/#/i.test(href);
const target = isInternal ? "_self" : aProps.target ?? "_blank";
return <a {...aProps} target={target} />;

View File

@@ -37,7 +37,7 @@ import Locale, { AllLangs, ALL_LANG_OPTIONS, Lang } from "../locales";
import { useNavigate } from "react-router-dom";
import chatStyle from "./chat.module.scss";
import { useEffect, useState } from "react";
import { useState } from "react";
import {
copyToClipboard,
downloadAs,
@@ -48,7 +48,6 @@ import { Updater } from "../typing";
import { ModelConfigList } from "./model-config";
import { FileName, Path } from "../constant";
import { BUILTIN_MASK_STORE } from "../masks";
import { nanoid } from "nanoid";
import {
DragDropContext,
Droppable,
@@ -56,6 +55,7 @@ import {
OnDragEndResponder,
} from "@hello-pangea/dnd";
import { getMessageTextContent } from "../utils";
import clsx from "clsx";
// drag and drop helper function
function reorder<T>(list: T[], startIndex: number, endIndex: number): T[] {
@@ -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) => {
@@ -163,12 +167,48 @@ export function MaskConfig(props: {
></input>
</ListItem>
{globalConfig.enableArtifacts && (
<ListItem
title={Locale.Mask.Config.Artifacts.Title}
subTitle={Locale.Mask.Config.Artifacts.SubTitle}
>
<input
aria-label={Locale.Mask.Config.Artifacts.Title}
type="checkbox"
checked={props.mask.enableArtifacts !== false}
onChange={(e) => {
props.updateMask((mask) => {
mask.enableArtifacts = e.currentTarget.checked;
});
}}
></input>
</ListItem>
)}
{globalConfig.enableCodeFold && (
<ListItem
title={Locale.Mask.Config.CodeFold.Title}
subTitle={Locale.Mask.Config.CodeFold.SubTitle}
>
<input
aria-label={Locale.Mask.Config.CodeFold.Title}
type="checkbox"
checked={props.mask.enableCodeFold !== false}
onChange={(e) => {
props.updateMask((mask) => {
mask.enableCodeFold = e.currentTarget.checked;
});
}}
></input>
</ListItem>
)}
{!props.shouldSyncFromGlobal ? (
<ListItem
title={Locale.Mask.Config.Share.Title}
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 +222,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) => {
@@ -404,16 +445,7 @@ export function MaskPage() {
const maskStore = useMaskStore();
const chatStore = useChatStore();
const [filterLang, setFilterLang] = useState<Lang | undefined>(
() => localStorage.getItem("Mask-language") as Lang | undefined,
);
useEffect(() => {
if (filterLang) {
localStorage.setItem("Mask-language", filterLang);
} else {
localStorage.removeItem("Mask-language");
}
}, [filterLang]);
const filterLang = maskStore.language;
const allMasks = maskStore
.getAll()
@@ -520,9 +552,9 @@ export function MaskPage() {
onChange={(e) => {
const value = e.currentTarget.value;
if (value === Locale.Settings.Lang.All) {
setFilterLang(undefined);
maskStore.setLanguage(undefined);
} else {
setFilterLang(value as Lang);
maskStore.setLanguage(value as Lang);
}
}}
>
@@ -557,7 +589,7 @@ export function MaskPage() {
</div>
<div className={styles["mask-title"]}>
<div className={styles["mask-name"]}>{m.name}</div>
<div className={styles["mask-info"] + " one-line"}>
<div className={clsx(styles["mask-info"], "one-line")}>
{`${Locale.Mask.Item.Info(m.context.length)} / ${
ALL_LANG_OPTIONS[m.lang]
} / ${m.modelConfig.model}`}

View File

@@ -8,6 +8,7 @@ import Locale from "../locales";
import styles from "./message-selector.module.scss";
import { getMessageTextContent } from "../utils";
import clsx from "clsx";
function useShiftRange() {
const [startIndex, setStartIndex] = useState<number>();
@@ -71,6 +72,7 @@ export function MessageSelector(props: {
defaultSelectAll?: boolean;
onSelected?: (messages: ChatMessage[]) => void;
}) {
const LATEST_COUNT = 4;
const chatStore = useChatStore();
const session = chatStore.currentSession();
const isValid = (m: ChatMessage) => m.content && !m.isError && !m.streaming;
@@ -141,15 +143,13 @@ export function MessageSelector(props: {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [startIndex, endIndex]);
const LATEST_COUNT = 4;
return (
<div className={styles["message-selector"]}>
<div className={styles["message-filter"]}>
<input
type="text"
placeholder={Locale.Select.Search}
className={styles["filter-item"] + " " + styles["search-bar"]}
className={clsx(styles["filter-item"], styles["search-bar"])}
value={searchInput}
onInput={(e) => {
setSearchInput(e.currentTarget.value);
@@ -196,9 +196,9 @@ export function MessageSelector(props: {
return (
<div
className={`${styles["message"]} ${
props.selection.has(m.id!) && styles["message-selected"]
}`}
className={clsx(styles["message"], {
[styles["message-selected"]]: props.selection.has(m.id!),
})}
key={i}
onClick={() => {
props.updateSelection((selection) => {
@@ -221,7 +221,7 @@ export function MessageSelector(props: {
<div className={styles["date"]}>
{new Date(m.date).toLocaleString()}
</div>
<div className={`${styles["content"]} one-line`}>
<div className={clsx(styles["content"], "one-line")}>
{getMessageTextContent(m)}
</div>
</div>

View File

@@ -0,0 +1,7 @@
.select-compress-model {
width: 60%;
select {
max-width: 100%;
white-space: normal;
}
}

View File

@@ -5,34 +5,48 @@ import Locale from "../locales";
import { InputRange } from "./input-range";
import { ListItem, Select } from "./ui-lib";
import { useAllModels } from "../utils/hooks";
import { groupBy } from "lodash-es";
import styles from "./model-config.module.scss";
import { getModelProvider } from "../utils/model";
export function ModelConfigList(props: {
modelConfig: ModelConfig;
updateConfig: (updater: (config: ModelConfig) => void) => void;
}) {
const allModels = useAllModels();
const groupModels = groupBy(
allModels.filter((v) => v.available),
"provider.providerName",
);
const value = `${props.modelConfig.model}@${props.modelConfig?.providerName}`;
const compressModelValue = `${props.modelConfig.compressModel}@${props.modelConfig?.compressProviderName}`;
return (
<>
<ListItem title={Locale.Settings.Model}>
<Select
aria-label={Locale.Settings.Model}
value={value}
align="left"
onChange={(e) => {
const [model, providerName] = e.currentTarget.value.split("@");
const [model, providerName] = getModelProvider(
e.currentTarget.value,
);
props.updateConfig((config) => {
config.model = ModalConfigValidator.model(model);
config.providerName = providerName as ServiceProvider;
});
}}
>
{allModels
.filter((v) => v.available)
.map((v, i) => (
<option value={`${v.name}@${v.provider?.providerName}`} key={i}>
{v.displayName}({v.provider?.providerName})
</option>
))}
{Object.keys(groupModels).map((providerName, index) => (
<optgroup label={providerName} key={index}>
{groupModels[providerName].map((v, i) => (
<option value={`${v.name}@${v.provider?.providerName}`} key={i}>
{v.displayName}
</option>
))}
</optgroup>
))}
</Select>
</ListItem>
<ListItem
@@ -40,6 +54,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 +74,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 +94,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 +117,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 +139,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 +161,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 +179,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 +196,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 +215,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 +231,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) =>
@@ -217,6 +241,33 @@ export function ModelConfigList(props: {
}
></input>
</ListItem>
<ListItem
title={Locale.Settings.CompressModel.Title}
subTitle={Locale.Settings.CompressModel.SubTitle}
>
<Select
className={styles["select-compress-model"]}
aria-label={Locale.Settings.CompressModel.Title}
value={compressModelValue}
onChange={(e) => {
const [model, providerName] = getModelProvider(
e.currentTarget.value,
);
props.updateConfig((config) => {
config.compressModel = ModalConfigValidator.model(model);
config.compressProviderName = providerName as ServiceProvider;
});
}}
>
{allModels
.filter((v) => v.available)
.map((v, i) => (
<option value={`${v.name}@${v.provider?.providerName}`} key={i}>
{v.displayName}({v.provider?.providerName})
</option>
))}
</Select>
</ListItem>
</>
);
}

View File

@@ -16,6 +16,7 @@ import { MaskAvatar } from "./mask";
import { useCommand } from "../command";
import { showConfirm } from "./ui-lib";
import { BUILTIN_MASK_STORE } from "../masks";
import clsx from "clsx";
function MaskItem(props: { mask: Mask; onClick?: () => void }) {
return (
@@ -24,7 +25,9 @@ function MaskItem(props: { mask: Mask; onClick?: () => void }) {
avatar={props.mask.avatar}
model={props.mask.modelConfig.model}
/>
<div className={styles["mask-name"] + " one-line"}>{props.mask.name}</div>
<div className={clsx(styles["mask-name"], "one-line")}>
{props.mask.name}
</div>
</div>
);
}

View File

@@ -0,0 +1,38 @@
.plugin-title {
font-weight: bolder;
font-size: 16px;
margin: 10px 0;
}
.plugin-content {
font-size: 14px;
font-family: inherit;
pre code {
max-height: 240px;
overflow-y: auto;
white-space: pre-wrap;
min-width: 280px;
}
}
.plugin-schema {
display: flex;
justify-content: flex-end;
flex-direction: row;
input {
margin-right: 20px;
@media screen and (max-width: 600px) {
margin-right: 0px;
}
}
@media screen and (max-width: 600px) {
flex-direction: column;
gap: 5px;
button {
padding: 10px;
}
}
}

370
app/components/plugin.tsx Normal file
View File

@@ -0,0 +1,370 @@
import { useDebouncedCallback } from "use-debounce";
import OpenAPIClientAxios from "openapi-client-axios";
import yaml from "js-yaml";
import { PLUGINS_REPO_URL } from "../constant";
import { IconButton } from "./button";
import { ErrorBoundary } from "./error";
import styles from "./mask.module.scss";
import pluginStyles from "./plugin.module.scss";
import EditIcon from "../icons/edit.svg";
import AddIcon from "../icons/add.svg";
import CloseIcon from "../icons/close.svg";
import DeleteIcon from "../icons/delete.svg";
import ConfirmIcon from "../icons/confirm.svg";
import ReloadIcon from "../icons/reload.svg";
import GithubIcon from "../icons/github.svg";
import { Plugin, usePluginStore, FunctionToolService } from "../store/plugin";
import {
PasswordInput,
List,
ListItem,
Modal,
showConfirm,
showToast,
} from "./ui-lib";
import Locale from "../locales";
import { useNavigate } from "react-router-dom";
import { useState } from "react";
import clsx from "clsx";
export function PluginPage() {
const navigate = useNavigate();
const pluginStore = usePluginStore();
const allPlugins = pluginStore.getAll();
const [searchPlugins, setSearchPlugins] = useState<Plugin[]>([]);
const [searchText, setSearchText] = useState("");
const plugins = searchText.length > 0 ? searchPlugins : allPlugins;
// refactored already, now it accurate
const onSearch = (text: string) => {
setSearchText(text);
if (text.length > 0) {
const result = allPlugins.filter(
(m) => m?.title.toLowerCase().includes(text.toLowerCase()),
);
setSearchPlugins(result);
} else {
setSearchPlugins(allPlugins);
}
};
const [editingPluginId, setEditingPluginId] = useState<string | undefined>();
const editingPlugin = pluginStore.get(editingPluginId);
const editingPluginTool = FunctionToolService.get(editingPlugin?.id);
const closePluginModal = () => setEditingPluginId(undefined);
const onChangePlugin = useDebouncedCallback((editingPlugin, e) => {
const content = e.target.innerText;
try {
const api = new OpenAPIClientAxios({
definition: yaml.load(content) as any,
});
api
.init()
.then(() => {
if (content != editingPlugin.content) {
pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
plugin.content = content;
const tool = FunctionToolService.add(plugin, true);
plugin.title = tool.api.definition.info.title;
plugin.version = tool.api.definition.info.version;
});
}
})
.catch((e) => {
console.error(e);
showToast(Locale.Plugin.EditModal.Error);
});
} catch (e) {
console.error(e);
showToast(Locale.Plugin.EditModal.Error);
}
}, 100).bind(null, editingPlugin);
const [loadUrl, setLoadUrl] = useState<string>("");
const loadFromUrl = (loadUrl: string) =>
fetch(loadUrl)
.catch((e) => {
const p = new URL(loadUrl);
return fetch(`/api/proxy/${p.pathname}?${p.search}`, {
headers: {
"X-Base-URL": p.origin,
},
});
})
.then((res) => res.text())
.then((content) => {
try {
return JSON.stringify(JSON.parse(content), null, " ");
} catch (e) {
return content;
}
})
.then((content) => {
pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
plugin.content = content;
const tool = FunctionToolService.add(plugin, true);
plugin.title = tool.api.definition.info.title;
plugin.version = tool.api.definition.info.version;
});
})
.catch((e) => {
showToast(Locale.Plugin.EditModal.Error);
});
return (
<ErrorBoundary>
<div className={styles["mask-page"]}>
<div className="window-header">
<div className="window-header-title">
<div className="window-header-main-title">
{Locale.Plugin.Page.Title}
</div>
<div className="window-header-submai-title">
{Locale.Plugin.Page.SubTitle(plugins.length)}
</div>
</div>
<div className="window-actions">
<div className="window-action-button">
<a
href={PLUGINS_REPO_URL}
target="_blank"
rel="noopener noreferrer"
>
<IconButton icon={<GithubIcon />} bordered />
</a>
</div>
<div className="window-action-button">
<IconButton
icon={<CloseIcon />}
bordered
onClick={() => navigate(-1)}
/>
</div>
</div>
</div>
<div className={styles["mask-page-body"]}>
<div className={styles["mask-filter"]}>
<input
type="text"
className={styles["search-bar"]}
placeholder={Locale.Plugin.Page.Search}
autoFocus
onInput={(e) => onSearch(e.currentTarget.value)}
/>
<IconButton
className={styles["mask-create"]}
icon={<AddIcon />}
text={Locale.Plugin.Page.Create}
bordered
onClick={() => {
const createdPlugin = pluginStore.create();
setEditingPluginId(createdPlugin.id);
}}
/>
</div>
<div>
{plugins.length == 0 && (
<div
style={{
display: "flex",
margin: "60px auto",
alignItems: "center",
justifyContent: "center",
}}
>
{Locale.Plugin.Page.Find}
<a
href={PLUGINS_REPO_URL}
target="_blank"
rel="noopener noreferrer"
style={{ marginLeft: 16 }}
>
<IconButton icon={<GithubIcon />} bordered />
</a>
</div>
)}
{plugins.map((m) => (
<div className={styles["mask-item"]} key={m.id}>
<div className={styles["mask-header"]}>
<div className={styles["mask-icon"]}></div>
<div className={styles["mask-title"]}>
<div className={styles["mask-name"]}>
{m.title}@<small>{m.version}</small>
</div>
<div className={clsx(styles["mask-info"], "one-line")}>
{Locale.Plugin.Item.Info(
FunctionToolService.add(m).length,
)}
</div>
</div>
</div>
<div className={styles["mask-actions"]}>
<IconButton
icon={<EditIcon />}
text={Locale.Plugin.Item.Edit}
onClick={() => setEditingPluginId(m.id)}
/>
{!m.builtin && (
<IconButton
icon={<DeleteIcon />}
text={Locale.Plugin.Item.Delete}
onClick={async () => {
if (
await showConfirm(Locale.Plugin.Item.DeleteConfirm)
) {
pluginStore.delete(m.id);
}
}}
/>
)}
</div>
</div>
))}
</div>
</div>
</div>
{editingPlugin && (
<div className="modal-mask">
<Modal
title={Locale.Plugin.EditModal.Title(editingPlugin?.builtin)}
onClose={closePluginModal}
actions={[
<IconButton
icon={<ConfirmIcon />}
text={Locale.UI.Confirm}
key="export"
bordered
onClick={() => setEditingPluginId("")}
/>,
]}
>
<List>
<ListItem title={Locale.Plugin.EditModal.Auth}>
<select
value={editingPlugin?.authType}
onChange={(e) => {
pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
plugin.authType = e.target.value;
});
}}
>
<option value="">{Locale.Plugin.Auth.None}</option>
<option value="bearer">{Locale.Plugin.Auth.Bearer}</option>
<option value="basic">{Locale.Plugin.Auth.Basic}</option>
<option value="custom">{Locale.Plugin.Auth.Custom}</option>
</select>
</ListItem>
{["bearer", "basic", "custom"].includes(
editingPlugin.authType as string,
) && (
<ListItem title={Locale.Plugin.Auth.Location}>
<select
value={editingPlugin?.authLocation}
onChange={(e) => {
pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
plugin.authLocation = e.target.value;
});
}}
>
<option value="header">
{Locale.Plugin.Auth.LocationHeader}
</option>
<option value="query">
{Locale.Plugin.Auth.LocationQuery}
</option>
<option value="body">
{Locale.Plugin.Auth.LocationBody}
</option>
</select>
</ListItem>
)}
{editingPlugin.authType == "custom" && (
<ListItem title={Locale.Plugin.Auth.CustomHeader}>
<input
type="text"
value={editingPlugin?.authHeader}
onChange={(e) => {
pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
plugin.authHeader = e.target.value;
});
}}
></input>
</ListItem>
)}
{["bearer", "basic", "custom"].includes(
editingPlugin.authType as string,
) && (
<ListItem title={Locale.Plugin.Auth.Token}>
<PasswordInput
type="text"
value={editingPlugin?.authToken}
onChange={(e) => {
pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
plugin.authToken = e.currentTarget.value;
});
}}
></PasswordInput>
</ListItem>
)}
</List>
<List>
<ListItem title={Locale.Plugin.EditModal.Content}>
<div className={pluginStyles["plugin-schema"]}>
<input
type="text"
style={{ minWidth: 200 }}
onInput={(e) => setLoadUrl(e.currentTarget.value)}
></input>
<IconButton
icon={<ReloadIcon />}
text={Locale.Plugin.EditModal.Load}
bordered
onClick={() => loadFromUrl(loadUrl)}
/>
</div>
</ListItem>
<ListItem
subTitle={
<div
className={clsx(
"markdown-body",
pluginStyles["plugin-content"],
)}
dir="auto"
>
<pre>
<code
contentEditable={true}
dangerouslySetInnerHTML={{
__html: editingPlugin.content,
}}
onBlur={onChangePlugin}
></code>
</pre>
</div>
}
></ListItem>
{editingPluginTool?.tools.map((tool, index) => (
<ListItem
key={index}
title={tool?.function?.name}
subTitle={tool?.function?.description}
/>
))}
</List>
</Modal>
</div>
)}
</ErrorBoundary>
);
}

View File

@@ -0,0 +1 @@
export * from "./realtime-chat";

View File

@@ -0,0 +1,74 @@
.realtime-chat {
width: 100%;
justify-content: center;
align-items: center;
position: relative;
display: flex;
flex-direction: column;
height: 100%;
padding: 20px;
box-sizing: border-box;
.circle-mic {
width: 150px;
height: 150px;
border-radius: 50%;
background: linear-gradient(to bottom right, #a0d8ef, #f0f8ff);
display: flex;
justify-content: center;
align-items: center;
}
.icon-center {
font-size: 24px;
}
.bottom-icons {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
position: absolute;
bottom: 20px;
box-sizing: border-box;
padding: 0 20px;
}
.icon-left,
.icon-right {
width: 46px;
height: 46px;
font-size: 36px;
background: var(--second);
border-radius: 50%;
padding: 2px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
&:hover {
opacity: 0.8;
}
}
&.mobile {
display: none;
}
}
.pulse {
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0% {
transform: scale(1);
opacity: 0.7;
}
50% {
transform: scale(1.1);
opacity: 1;
}
100% {
transform: scale(1);
opacity: 0.7;
}
}

View File

@@ -0,0 +1,359 @@
import VoiceIcon from "@/app/icons/voice.svg";
import VoiceOffIcon from "@/app/icons/voice-off.svg";
import PowerIcon from "@/app/icons/power.svg";
import styles from "./realtime-chat.module.scss";
import clsx from "clsx";
import { useState, useRef, useEffect } from "react";
import { useChatStore, createMessage, useAppConfig } from "@/app/store";
import { IconButton } from "@/app/components/button";
import {
Modality,
RTClient,
RTInputAudioItem,
RTResponse,
TurnDetection,
} from "rt-client";
import { AudioHandler } from "@/app/lib/audio";
import { uploadImage } from "@/app/utils/chat";
import { VoicePrint } from "@/app/components/voice-print";
interface RealtimeChatProps {
onClose?: () => void;
onStartVoice?: () => void;
onPausedVoice?: () => void;
}
export function RealtimeChat({
onClose,
onStartVoice,
onPausedVoice,
}: RealtimeChatProps) {
const chatStore = useChatStore();
const session = chatStore.currentSession();
const config = useAppConfig();
const [status, setStatus] = useState("");
const [isRecording, setIsRecording] = useState(false);
const [isConnected, setIsConnected] = useState(false);
const [isConnecting, setIsConnecting] = useState(false);
const [modality, setModality] = useState("audio");
const [useVAD, setUseVAD] = useState(true);
const [frequencies, setFrequencies] = useState<Uint8Array | undefined>();
const clientRef = useRef<RTClient | null>(null);
const audioHandlerRef = useRef<AudioHandler | null>(null);
const initRef = useRef(false);
const temperature = config.realtimeConfig.temperature;
const apiKey = config.realtimeConfig.apiKey;
const model = config.realtimeConfig.model;
const azure = config.realtimeConfig.provider === "Azure";
const azureEndpoint = config.realtimeConfig.azure.endpoint;
const azureDeployment = config.realtimeConfig.azure.deployment;
const voice = config.realtimeConfig.voice;
const handleConnect = async () => {
if (isConnecting) return;
if (!isConnected) {
try {
setIsConnecting(true);
clientRef.current = azure
? new RTClient(
new URL(azureEndpoint),
{ key: apiKey },
{ deployment: azureDeployment },
)
: new RTClient({ key: apiKey }, { model });
const modalities: Modality[] =
modality === "audio" ? ["text", "audio"] : ["text"];
const turnDetection: TurnDetection = useVAD
? { type: "server_vad" }
: null;
await clientRef.current.configure({
instructions: "",
voice,
input_audio_transcription: { model: "whisper-1" },
turn_detection: turnDetection,
tools: [],
temperature,
modalities,
});
startResponseListener();
setIsConnected(true);
// TODO
// try {
// const recentMessages = chatStore.getMessagesWithMemory();
// for (const message of recentMessages) {
// const { role, content } = message;
// if (typeof content === "string") {
// await clientRef.current.sendItem({
// type: "message",
// role: role as any,
// content: [
// {
// type: (role === "assistant" ? "text" : "input_text") as any,
// text: content as string,
// },
// ],
// });
// }
// }
// // await clientRef.current.generateResponse();
// } catch (error) {
// console.error("Set message failed:", error);
// }
} catch (error) {
console.error("Connection failed:", error);
setStatus("Connection failed");
} finally {
setIsConnecting(false);
}
} else {
await disconnect();
}
};
const disconnect = async () => {
if (clientRef.current) {
try {
await clientRef.current.close();
clientRef.current = null;
setIsConnected(false);
} catch (error) {
console.error("Disconnect failed:", error);
}
}
};
const startResponseListener = async () => {
if (!clientRef.current) return;
try {
for await (const serverEvent of clientRef.current.events()) {
if (serverEvent.type === "response") {
await handleResponse(serverEvent);
} else if (serverEvent.type === "input_audio") {
await handleInputAudio(serverEvent);
}
}
} catch (error) {
if (clientRef.current) {
console.error("Response iteration error:", error);
}
}
};
const handleResponse = async (response: RTResponse) => {
for await (const item of response) {
if (item.type === "message" && item.role === "assistant") {
const botMessage = createMessage({
role: item.role,
content: "",
});
// add bot message first
chatStore.updateTargetSession(session, (session) => {
session.messages = session.messages.concat([botMessage]);
});
let hasAudio = false;
for await (const content of item) {
if (content.type === "text") {
for await (const text of content.textChunks()) {
botMessage.content += text;
}
} else if (content.type === "audio") {
const textTask = async () => {
for await (const text of content.transcriptChunks()) {
botMessage.content += text;
}
};
const audioTask = async () => {
audioHandlerRef.current?.startStreamingPlayback();
for await (const audio of content.audioChunks()) {
hasAudio = true;
audioHandlerRef.current?.playChunk(audio);
}
};
await Promise.all([textTask(), audioTask()]);
}
// update message.content
chatStore.updateTargetSession(session, (session) => {
session.messages = session.messages.concat();
});
}
if (hasAudio) {
// upload audio get audio_url
const blob = audioHandlerRef.current?.savePlayFile();
uploadImage(blob!).then((audio_url) => {
botMessage.audio_url = audio_url;
// update text and audio_url
chatStore.updateTargetSession(session, (session) => {
session.messages = session.messages.concat();
});
});
}
}
}
};
const handleInputAudio = async (item: RTInputAudioItem) => {
await item.waitForCompletion();
if (item.transcription) {
const userMessage = createMessage({
role: "user",
content: item.transcription,
});
chatStore.updateTargetSession(session, (session) => {
session.messages = session.messages.concat([userMessage]);
});
// save input audio_url, and update session
const { audioStartMillis, audioEndMillis } = item;
// upload audio get audio_url
const blob = audioHandlerRef.current?.saveRecordFile(
audioStartMillis,
audioEndMillis,
);
uploadImage(blob!).then((audio_url) => {
userMessage.audio_url = audio_url;
chatStore.updateTargetSession(session, (session) => {
session.messages = session.messages.concat();
});
});
}
// stop streaming play after get input audio.
audioHandlerRef.current?.stopStreamingPlayback();
};
const toggleRecording = async () => {
if (!isRecording && clientRef.current) {
try {
if (!audioHandlerRef.current) {
audioHandlerRef.current = new AudioHandler();
await audioHandlerRef.current.initialize();
}
await audioHandlerRef.current.startRecording(async (chunk) => {
await clientRef.current?.sendAudio(chunk);
});
setIsRecording(true);
} catch (error) {
console.error("Failed to start recording:", error);
}
} else if (audioHandlerRef.current) {
try {
audioHandlerRef.current.stopRecording();
if (!useVAD) {
const inputAudio = await clientRef.current?.commitAudio();
await handleInputAudio(inputAudio!);
await clientRef.current?.generateResponse();
}
setIsRecording(false);
} catch (error) {
console.error("Failed to stop recording:", error);
}
}
};
useEffect(() => {
// 防止重复初始化
if (initRef.current) return;
initRef.current = true;
const initAudioHandler = async () => {
const handler = new AudioHandler();
await handler.initialize();
audioHandlerRef.current = handler;
await handleConnect();
await toggleRecording();
};
initAudioHandler().catch((error) => {
setStatus(error);
console.error(error);
});
return () => {
if (isRecording) {
toggleRecording();
}
audioHandlerRef.current?.close().catch(console.error);
disconnect();
};
}, []);
useEffect(() => {
let animationFrameId: number;
if (isConnected && isRecording) {
const animationFrame = () => {
if (audioHandlerRef.current) {
const freqData = audioHandlerRef.current.getByteFrequencyData();
setFrequencies(freqData);
}
animationFrameId = requestAnimationFrame(animationFrame);
};
animationFrameId = requestAnimationFrame(animationFrame);
} else {
setFrequencies(undefined);
}
return () => {
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
}
};
}, [isConnected, isRecording]);
// update session params
useEffect(() => {
clientRef.current?.configure({ voice });
}, [voice]);
useEffect(() => {
clientRef.current?.configure({ temperature });
}, [temperature]);
const handleClose = async () => {
onClose?.();
if (isRecording) {
await toggleRecording();
}
disconnect().catch(console.error);
};
return (
<div className={styles["realtime-chat"]}>
<div
className={clsx(styles["circle-mic"], {
[styles["pulse"]]: isRecording,
})}
>
<VoicePrint frequencies={frequencies} isActive={isRecording} />
</div>
<div className={styles["bottom-icons"]}>
<div>
<IconButton
icon={isRecording ? <VoiceIcon /> : <VoiceOffIcon />}
onClick={toggleRecording}
disabled={!isConnected}
shadow
bordered
/>
</div>
<div className={styles["icon-center"]}>{status}</div>
<div>
<IconButton
icon={<PowerIcon />}
onClick={handleClose}
shadow
bordered
/>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,173 @@
import { RealtimeConfig } from "@/app/store";
import Locale from "@/app/locales";
import { ListItem, Select, PasswordInput } from "@/app/components/ui-lib";
import { InputRange } from "@/app/components/input-range";
import { Voice } from "rt-client";
import { ServiceProvider } from "@/app/constant";
const providers = [ServiceProvider.OpenAI, ServiceProvider.Azure];
const models = ["gpt-4o-realtime-preview-2024-10-01"];
const voice = ["alloy", "shimmer", "echo"];
export function RealtimeConfigList(props: {
realtimeConfig: RealtimeConfig;
updateConfig: (updater: (config: RealtimeConfig) => void) => void;
}) {
const azureConfigComponent = props.realtimeConfig.provider ===
ServiceProvider.Azure && (
<>
<ListItem
title={Locale.Settings.Realtime.Azure.Endpoint.Title}
subTitle={Locale.Settings.Realtime.Azure.Endpoint.SubTitle}
>
<input
value={props.realtimeConfig?.azure?.endpoint}
type="text"
placeholder={Locale.Settings.Realtime.Azure.Endpoint.Title}
onChange={(e) => {
props.updateConfig(
(config) => (config.azure.endpoint = e.currentTarget.value),
);
}}
/>
</ListItem>
<ListItem
title={Locale.Settings.Realtime.Azure.Deployment.Title}
subTitle={Locale.Settings.Realtime.Azure.Deployment.SubTitle}
>
<input
value={props.realtimeConfig?.azure?.deployment}
type="text"
placeholder={Locale.Settings.Realtime.Azure.Deployment.Title}
onChange={(e) => {
props.updateConfig(
(config) => (config.azure.deployment = e.currentTarget.value),
);
}}
/>
</ListItem>
</>
);
return (
<>
<ListItem
title={Locale.Settings.Realtime.Enable.Title}
subTitle={Locale.Settings.Realtime.Enable.SubTitle}
>
<input
type="checkbox"
checked={props.realtimeConfig.enable}
onChange={(e) =>
props.updateConfig(
(config) => (config.enable = e.currentTarget.checked),
)
}
></input>
</ListItem>
{props.realtimeConfig.enable && (
<>
<ListItem
title={Locale.Settings.Realtime.Provider.Title}
subTitle={Locale.Settings.Realtime.Provider.SubTitle}
>
<Select
aria-label={Locale.Settings.Realtime.Provider.Title}
value={props.realtimeConfig.provider}
onChange={(e) => {
props.updateConfig(
(config) =>
(config.provider = e.target.value as ServiceProvider),
);
}}
>
{providers.map((v, i) => (
<option value={v} key={i}>
{v}
</option>
))}
</Select>
</ListItem>
<ListItem
title={Locale.Settings.Realtime.Model.Title}
subTitle={Locale.Settings.Realtime.Model.SubTitle}
>
<Select
aria-label={Locale.Settings.Realtime.Model.Title}
value={props.realtimeConfig.model}
onChange={(e) => {
props.updateConfig((config) => (config.model = e.target.value));
}}
>
{models.map((v, i) => (
<option value={v} key={i}>
{v}
</option>
))}
</Select>
</ListItem>
<ListItem
title={Locale.Settings.Realtime.ApiKey.Title}
subTitle={Locale.Settings.Realtime.ApiKey.SubTitle}
>
<PasswordInput
aria={Locale.Settings.ShowPassword}
aria-label={Locale.Settings.Realtime.ApiKey.Title}
value={props.realtimeConfig.apiKey}
type="text"
placeholder={Locale.Settings.Realtime.ApiKey.Placeholder}
onChange={(e) => {
props.updateConfig(
(config) => (config.apiKey = e.currentTarget.value),
);
}}
/>
</ListItem>
{azureConfigComponent}
<ListItem
title={Locale.Settings.TTS.Voice.Title}
subTitle={Locale.Settings.TTS.Voice.SubTitle}
>
<Select
value={props.realtimeConfig.voice}
onChange={(e) => {
props.updateConfig(
(config) => (config.voice = e.currentTarget.value as Voice),
);
}}
>
{voice.map((v, i) => (
<option value={v} key={i}>
{v}
</option>
))}
</Select>
</ListItem>
<ListItem
title={Locale.Settings.Realtime.Temperature.Title}
subTitle={Locale.Settings.Realtime.Temperature.SubTitle}
>
<InputRange
aria={Locale.Settings.Temperature.Title}
value={props.realtimeConfig?.temperature?.toFixed(1)}
min="0.6"
max="1"
step="0.1"
onChange={(e) => {
props.updateConfig(
(config) =>
(config.temperature = e.currentTarget.valueAsNumber),
);
}}
></InputRange>
</ListItem>
</>
)}
</>
);
}

View File

@@ -4,6 +4,7 @@ import { Select } from "@/app/components/ui-lib";
import { IconButton } from "@/app/components/button";
import Locale from "@/app/locales";
import { useSdStore } from "@/app/store/sd";
import clsx from "clsx";
export const params = [
{
@@ -136,7 +137,7 @@ export function ControlParamItem(props: {
className?: string;
}) {
return (
<div className={styles["ctrl-param-item"] + ` ${props.className || ""}`}>
<div className={clsx(styles["ctrl-param-item"], props.className)}>
<div className={styles["ctrl-param-item-header"]}>
<div className={styles["ctrl-param-item-title"]}>
<div>
@@ -192,6 +193,7 @@ export function ControlParam(props: {
required={item.required}
>
<Select
aria-label={item.name}
value={props.data[item.value]}
onChange={(e) => {
props.onChange(item.value, e.currentTarget.value);
@@ -216,6 +218,7 @@ export function ControlParam(props: {
required={item.required}
>
<input
aria-label={item.name}
type="number"
min={item.min}
max={item.max}
@@ -235,6 +238,7 @@ export function ControlParam(props: {
required={item.required}
>
<input
aria-label={item.name}
type="text"
value={props.data[item.value]}
style={{ maxWidth: "100%", width: "100%" }}

View File

@@ -36,6 +36,7 @@ import { removeImage } from "@/app/utils/chat";
import { SideBar } from "./sd-sidebar";
import { WindowContent } from "@/app/components/home";
import { params } from "./sd-panel";
import clsx from "clsx";
function getSdTaskStatus(item: any) {
let s: string;
@@ -104,7 +105,7 @@ export function Sd() {
return (
<>
<SideBar className={isSd ? homeStyles["sidebar-show"] : ""} />
<SideBar className={clsx({ [homeStyles["sidebar-show"]]: isSd })} />
<WindowContent>
<div className={chatStyles.chat} key={"1"}>
<div className="window-header" data-tauri-drag-region>
@@ -121,7 +122,10 @@ export function Sd() {
</div>
)}
<div
className={`window-header-title ${chatStyles["chat-body-title"]}`}
className={clsx(
"window-header-title",
chatStyles["chat-body-title"],
)}
>
<div className={`window-header-main-title`}>Stability AI</div>
<div className="window-header-sub-title">
@@ -133,6 +137,7 @@ export function Sd() {
{showMaxIcon && (
<div className="window-action-button">
<IconButton
aria={Locale.Chat.Actions.FullScreen}
icon={config.tightBorder ? <MinIcon /> : <MaxIcon />}
bordered
onClick={() => {

View File

@@ -0,0 +1,167 @@
import { useState, useEffect, useRef, useCallback } from "react";
import { ErrorBoundary } from "./error";
import styles from "./mask.module.scss";
import { useNavigate } from "react-router-dom";
import { IconButton } from "./button";
import CloseIcon from "../icons/close.svg";
import EyeIcon from "../icons/eye.svg";
import Locale from "../locales";
import { Path } from "../constant";
import { useChatStore } from "../store";
type Item = {
id: number;
name: string;
content: string;
};
export function SearchChatPage() {
const navigate = useNavigate();
const chatStore = useChatStore();
const sessions = chatStore.sessions;
const selectSession = chatStore.selectSession;
const [searchResults, setSearchResults] = useState<Item[]>([]);
const previousValueRef = useRef<string>("");
const searchInputRef = useRef<HTMLInputElement>(null);
const doSearch = useCallback((text: string) => {
const lowerCaseText = text.toLowerCase();
const results: Item[] = [];
sessions.forEach((session, index) => {
const fullTextContents: string[] = [];
session.messages.forEach((message) => {
const content = message.content as string;
if (!content.toLowerCase || content === "") return;
const lowerCaseContent = content.toLowerCase();
// full text search
let pos = lowerCaseContent.indexOf(lowerCaseText);
while (pos !== -1) {
const start = Math.max(0, pos - 35);
const end = Math.min(content.length, pos + lowerCaseText.length + 35);
fullTextContents.push(content.substring(start, end));
pos = lowerCaseContent.indexOf(
lowerCaseText,
pos + lowerCaseText.length,
);
}
});
if (fullTextContents.length > 0) {
results.push({
id: index,
name: session.topic,
content: fullTextContents.join("... "), // concat content with...
});
}
});
// sort by length of matching content
results.sort((a, b) => b.content.length - a.content.length);
return results;
}, []);
useEffect(() => {
const intervalId = setInterval(() => {
if (searchInputRef.current) {
const currentValue = searchInputRef.current.value;
if (currentValue !== previousValueRef.current) {
if (currentValue.length > 0) {
const result = doSearch(currentValue);
setSearchResults(result);
}
previousValueRef.current = currentValue;
}
}
}, 1000);
// Cleanup the interval on component unmount
return () => clearInterval(intervalId);
}, [doSearch]);
return (
<ErrorBoundary>
<div className={styles["mask-page"]}>
{/* header */}
<div className="window-header">
<div className="window-header-title">
<div className="window-header-main-title">
{Locale.SearchChat.Page.Title}
</div>
<div className="window-header-submai-title">
{Locale.SearchChat.Page.SubTitle(searchResults.length)}
</div>
</div>
<div className="window-actions">
<div className="window-action-button">
<IconButton
icon={<CloseIcon />}
bordered
onClick={() => navigate(-1)}
/>
</div>
</div>
</div>
<div className={styles["mask-page-body"]}>
<div className={styles["mask-filter"]}>
{/**搜索输入框 */}
<input
type="text"
className={styles["search-bar"]}
placeholder={Locale.SearchChat.Page.Search}
autoFocus
ref={searchInputRef}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
const searchText = e.currentTarget.value;
if (searchText.length > 0) {
const result = doSearch(searchText);
setSearchResults(result);
}
}
}}
/>
</div>
<div>
{searchResults.map((item) => (
<div
className={styles["mask-item"]}
key={item.id}
onClick={() => {
navigate(Path.Chat);
selectSession(item.id);
}}
style={{ cursor: "pointer" }}
>
{/** 搜索匹配的文本 */}
<div className={styles["mask-header"]}>
<div className={styles["mask-title"]}>
<div className={styles["mask-name"]}>{item.name}</div>
{item.content.slice(0, 70)}
</div>
</div>
{/** 操作按钮 */}
<div className={styles["mask-actions"]}>
<IconButton
icon={<EyeIcon />}
text={Locale.SearchChat.Item.View}
/>
</div>
</div>
))}
</div>
</div>
</div>
</ErrorBoundary>
);
}

View File

@@ -72,3 +72,9 @@
}
}
}
.subtitle-button {
button {
overflow:visible ;
}
}

View File

@@ -9,6 +9,7 @@ import CopyIcon from "../icons/copy.svg";
import ClearIcon from "../icons/clear.svg";
import LoadingIcon from "../icons/three-dots.svg";
import EditIcon from "../icons/edit.svg";
import FireIcon from "../icons/fire.svg";
import EyeIcon from "../icons/eye.svg";
import DownloadIcon from "../icons/download.svg";
import UploadIcon from "../icons/upload.svg";
@@ -18,7 +19,7 @@ import ConfirmIcon from "../icons/confirm.svg";
import ConnectionIcon from "../icons/connection.svg";
import CloudSuccessIcon from "../icons/cloud-success.svg";
import CloudFailIcon from "../icons/cloud-fail.svg";
import { trackSettingsPageGuideToCPaymentClick } from "../utils/auth-settings-events";
import {
Input,
List,
@@ -48,7 +49,7 @@ import Locale, {
changeLang,
getLang,
} from "../locales";
import { copyToClipboard } from "../utils";
import { copyToClipboard, clientUpdate, semverCompare } from "../utils";
import Link from "next/link";
import {
Anthropic,
@@ -58,6 +59,7 @@ import {
ByteDance,
Alibaba,
Moonshot,
XAI,
Google,
GoogleSafetySettingsThreshold,
OPENAI_BASE_URL,
@@ -69,6 +71,8 @@ import {
UPDATE_URL,
Stability,
Iflytek,
SAAS_CHAT_URL,
ChatGLM,
} from "../constant";
import { Prompt, SearchService, usePromptStore } from "../store/prompt";
import { ErrorBoundary } from "./error";
@@ -80,6 +84,8 @@ import { useSyncStore } from "../store/sync";
import { nanoid } from "nanoid";
import { useMaskStore } from "../store/mask";
import { ProviderType } from "../utils/cloud";
import { TTSConfigList } from "./tts-config";
import { RealtimeConfigList } from "./realtime-chat/realtime-config";
function EditPromptModal(props: { id: string; onClose: () => void }) {
const promptStore = usePromptStore();
@@ -246,6 +252,7 @@ function DangerItems() {
subTitle={Locale.Settings.Danger.Reset.SubTitle}
>
<IconButton
aria={Locale.Settings.Danger.Reset.Title}
text={Locale.Settings.Danger.Reset.Action}
onClick={async () => {
if (await showConfirm(Locale.Settings.Danger.Reset.Confirm)) {
@@ -260,6 +267,7 @@ function DangerItems() {
subTitle={Locale.Settings.Danger.Clear.SubTitle}
>
<IconButton
aria={Locale.Settings.Danger.Clear.Title}
text={Locale.Settings.Danger.Clear.Action}
onClick={async () => {
if (await showConfirm(Locale.Settings.Danger.Clear.Confirm)) {
@@ -513,6 +521,7 @@ function SyncItems() {
>
<div style={{ display: "flex" }}>
<IconButton
aria={Locale.Settings.Sync.CloudState + Locale.UI.Config}
icon={<ConfigIcon />}
text={Locale.UI.Config}
onClick={() => {
@@ -543,6 +552,7 @@ function SyncItems() {
>
<div style={{ display: "flex" }}>
<IconButton
aria={Locale.Settings.Sync.LocalState + Locale.UI.Export}
icon={<UploadIcon />}
text={Locale.UI.Export}
onClick={() => {
@@ -550,6 +560,7 @@ function SyncItems() {
}}
/>
<IconButton
aria={Locale.Settings.Sync.LocalState + Locale.UI.Import}
icon={<DownloadIcon />}
text={Locale.UI.Import}
onClick={() => {
@@ -577,7 +588,7 @@ export function Settings() {
const [checkingUpdate, setCheckingUpdate] = useState(false);
const currentVersion = updateStore.formatVersion(updateStore.version);
const remoteId = updateStore.formatVersion(updateStore.remoteVersion);
const hasNewVersion = currentVersion !== remoteId;
const hasNewVersion = semverCompare(currentVersion, remoteId) === -1;
const updateUrl = getClientConfig()?.isApp ? RELEASE_URL : UPDATE_URL;
function checkUpdate(force = false) {
@@ -680,6 +691,31 @@ export function Settings() {
</ListItem>
);
const saasStartComponent = (
<ListItem
className={styles["subtitle-button"]}
title={
Locale.Settings.Access.SaasStart.Title +
`${Locale.Settings.Access.SaasStart.Label}`
}
subTitle={Locale.Settings.Access.SaasStart.SubTitle}
>
<IconButton
aria={
Locale.Settings.Access.SaasStart.Title +
Locale.Settings.Access.SaasStart.ChatNow
}
icon={<FireIcon />}
type={"primary"}
text={Locale.Settings.Access.SaasStart.ChatNow}
onClick={() => {
trackSettingsPageGuideToCPaymentClick();
window.location.href = SAAS_CHAT_URL;
}}
/>
</ListItem>
);
const useCustomConfigComponent = // Conditionally render the following ListItem based on clientConfig.isApp
!clientConfig?.isApp && ( // only show if isApp is false
<ListItem
@@ -687,6 +723,7 @@ export function Settings() {
subTitle={Locale.Settings.Access.CustomEndpoint.SubTitle}
>
<input
aria-label={Locale.Settings.Access.CustomEndpoint.Title}
type="checkbox"
checked={accessStore.useCustomConfig}
onChange={(e) =>
@@ -706,6 +743,7 @@ export function Settings() {
subTitle={Locale.Settings.Access.OpenAI.Endpoint.SubTitle}
>
<input
aria-label={Locale.Settings.Access.OpenAI.Endpoint.Title}
type="text"
value={accessStore.openaiUrl}
placeholder={OPENAI_BASE_URL}
@@ -721,6 +759,8 @@ export function Settings() {
subTitle={Locale.Settings.Access.OpenAI.ApiKey.SubTitle}
>
<PasswordInput
aria={Locale.Settings.ShowPassword}
aria-label={Locale.Settings.Access.OpenAI.ApiKey.Title}
value={accessStore.openaiApiKey}
type="text"
placeholder={Locale.Settings.Access.OpenAI.ApiKey.Placeholder}
@@ -744,6 +784,7 @@ export function Settings() {
}
>
<input
aria-label={Locale.Settings.Access.Azure.Endpoint.Title}
type="text"
value={accessStore.azureUrl}
placeholder={Azure.ExampleEndpoint}
@@ -759,6 +800,7 @@ export function Settings() {
subTitle={Locale.Settings.Access.Azure.ApiKey.SubTitle}
>
<PasswordInput
aria-label={Locale.Settings.Access.Azure.ApiKey.Title}
value={accessStore.azureApiKey}
type="text"
placeholder={Locale.Settings.Access.Azure.ApiKey.Placeholder}
@@ -774,6 +816,7 @@ export function Settings() {
subTitle={Locale.Settings.Access.Azure.ApiVerion.SubTitle}
>
<input
aria-label={Locale.Settings.Access.Azure.ApiVerion.Title}
type="text"
value={accessStore.azureApiVersion}
placeholder="2023-08-01-preview"
@@ -798,6 +841,7 @@ export function Settings() {
}
>
<input
aria-label={Locale.Settings.Access.Google.Endpoint.Title}
type="text"
value={accessStore.googleUrl}
placeholder={Google.ExampleEndpoint}
@@ -813,6 +857,7 @@ export function Settings() {
subTitle={Locale.Settings.Access.Google.ApiKey.SubTitle}
>
<PasswordInput
aria-label={Locale.Settings.Access.Google.ApiKey.Title}
value={accessStore.googleApiKey}
type="text"
placeholder={Locale.Settings.Access.Google.ApiKey.Placeholder}
@@ -828,6 +873,7 @@ export function Settings() {
subTitle={Locale.Settings.Access.Google.ApiVersion.SubTitle}
>
<input
aria-label={Locale.Settings.Access.Google.ApiVersion.Title}
type="text"
value={accessStore.googleApiVersion}
placeholder="2023-08-01-preview"
@@ -843,6 +889,7 @@ export function Settings() {
subTitle={Locale.Settings.Access.Google.GoogleSafetySettings.SubTitle}
>
<Select
aria-label={Locale.Settings.Access.Google.GoogleSafetySettings.Title}
value={accessStore.googleSafetySettings}
onChange={(e) => {
accessStore.update(
@@ -873,6 +920,7 @@ export function Settings() {
}
>
<input
aria-label={Locale.Settings.Access.Anthropic.Endpoint.Title}
type="text"
value={accessStore.anthropicUrl}
placeholder={Anthropic.ExampleEndpoint}
@@ -888,6 +936,7 @@ export function Settings() {
subTitle={Locale.Settings.Access.Anthropic.ApiKey.SubTitle}
>
<PasswordInput
aria-label={Locale.Settings.Access.Anthropic.ApiKey.Title}
value={accessStore.anthropicApiKey}
type="text"
placeholder={Locale.Settings.Access.Anthropic.ApiKey.Placeholder}
@@ -903,6 +952,7 @@ export function Settings() {
subTitle={Locale.Settings.Access.Anthropic.ApiVerion.SubTitle}
>
<input
aria-label={Locale.Settings.Access.Anthropic.ApiVerion.Title}
type="text"
value={accessStore.anthropicApiVersion}
placeholder={Anthropic.Vision}
@@ -924,6 +974,7 @@ export function Settings() {
subTitle={Locale.Settings.Access.Baidu.Endpoint.SubTitle}
>
<input
aria-label={Locale.Settings.Access.Baidu.Endpoint.Title}
type="text"
value={accessStore.baiduUrl}
placeholder={Baidu.ExampleEndpoint}
@@ -939,6 +990,7 @@ export function Settings() {
subTitle={Locale.Settings.Access.Baidu.ApiKey.SubTitle}
>
<PasswordInput
aria-label={Locale.Settings.Access.Baidu.ApiKey.Title}
value={accessStore.baiduApiKey}
type="text"
placeholder={Locale.Settings.Access.Baidu.ApiKey.Placeholder}
@@ -954,6 +1006,7 @@ export function Settings() {
subTitle={Locale.Settings.Access.Baidu.SecretKey.SubTitle}
>
<PasswordInput
aria-label={Locale.Settings.Access.Baidu.SecretKey.Title}
value={accessStore.baiduSecretKey}
type="text"
placeholder={Locale.Settings.Access.Baidu.SecretKey.Placeholder}
@@ -975,6 +1028,7 @@ export function Settings() {
subTitle={Locale.Settings.Access.Tencent.Endpoint.SubTitle}
>
<input
aria-label={Locale.Settings.Access.Tencent.Endpoint.Title}
type="text"
value={accessStore.tencentUrl}
placeholder={Tencent.ExampleEndpoint}
@@ -990,6 +1044,7 @@ export function Settings() {
subTitle={Locale.Settings.Access.Tencent.ApiKey.SubTitle}
>
<PasswordInput
aria-label={Locale.Settings.Access.Tencent.ApiKey.Title}
value={accessStore.tencentSecretId}
type="text"
placeholder={Locale.Settings.Access.Tencent.ApiKey.Placeholder}
@@ -1005,6 +1060,7 @@ export function Settings() {
subTitle={Locale.Settings.Access.Tencent.SecretKey.SubTitle}
>
<PasswordInput
aria-label={Locale.Settings.Access.Tencent.SecretKey.Title}
value={accessStore.tencentSecretKey}
type="text"
placeholder={Locale.Settings.Access.Tencent.SecretKey.Placeholder}
@@ -1029,6 +1085,7 @@ export function Settings() {
}
>
<input
aria-label={Locale.Settings.Access.ByteDance.Endpoint.Title}
type="text"
value={accessStore.bytedanceUrl}
placeholder={ByteDance.ExampleEndpoint}
@@ -1044,6 +1101,7 @@ export function Settings() {
subTitle={Locale.Settings.Access.ByteDance.ApiKey.SubTitle}
>
<PasswordInput
aria-label={Locale.Settings.Access.ByteDance.ApiKey.Title}
value={accessStore.bytedanceApiKey}
type="text"
placeholder={Locale.Settings.Access.ByteDance.ApiKey.Placeholder}
@@ -1068,6 +1126,7 @@ export function Settings() {
}
>
<input
aria-label={Locale.Settings.Access.Alibaba.Endpoint.Title}
type="text"
value={accessStore.alibabaUrl}
placeholder={Alibaba.ExampleEndpoint}
@@ -1083,6 +1142,7 @@ export function Settings() {
subTitle={Locale.Settings.Access.Alibaba.ApiKey.SubTitle}
>
<PasswordInput
aria-label={Locale.Settings.Access.Alibaba.ApiKey.Title}
value={accessStore.alibabaApiKey}
type="text"
placeholder={Locale.Settings.Access.Alibaba.ApiKey.Placeholder}
@@ -1107,6 +1167,7 @@ export function Settings() {
}
>
<input
aria-label={Locale.Settings.Access.Moonshot.Endpoint.Title}
type="text"
value={accessStore.moonshotUrl}
placeholder={Moonshot.ExampleEndpoint}
@@ -1122,6 +1183,7 @@ export function Settings() {
subTitle={Locale.Settings.Access.Moonshot.ApiKey.SubTitle}
>
<PasswordInput
aria-label={Locale.Settings.Access.Moonshot.ApiKey.Title}
value={accessStore.moonshotApiKey}
type="text"
placeholder={Locale.Settings.Access.Moonshot.ApiKey.Placeholder}
@@ -1135,6 +1197,86 @@ export function Settings() {
</>
);
const XAIConfigComponent = accessStore.provider === ServiceProvider.XAI && (
<>
<ListItem
title={Locale.Settings.Access.XAI.Endpoint.Title}
subTitle={
Locale.Settings.Access.XAI.Endpoint.SubTitle + XAI.ExampleEndpoint
}
>
<input
aria-label={Locale.Settings.Access.XAI.Endpoint.Title}
type="text"
value={accessStore.xaiUrl}
placeholder={XAI.ExampleEndpoint}
onChange={(e) =>
accessStore.update(
(access) => (access.xaiUrl = e.currentTarget.value),
)
}
></input>
</ListItem>
<ListItem
title={Locale.Settings.Access.XAI.ApiKey.Title}
subTitle={Locale.Settings.Access.XAI.ApiKey.SubTitle}
>
<PasswordInput
aria-label={Locale.Settings.Access.XAI.ApiKey.Title}
value={accessStore.xaiApiKey}
type="text"
placeholder={Locale.Settings.Access.XAI.ApiKey.Placeholder}
onChange={(e) => {
accessStore.update(
(access) => (access.xaiApiKey = e.currentTarget.value),
);
}}
/>
</ListItem>
</>
);
const chatglmConfigComponent = accessStore.provider ===
ServiceProvider.ChatGLM && (
<>
<ListItem
title={Locale.Settings.Access.ChatGLM.Endpoint.Title}
subTitle={
Locale.Settings.Access.ChatGLM.Endpoint.SubTitle +
ChatGLM.ExampleEndpoint
}
>
<input
aria-label={Locale.Settings.Access.ChatGLM.Endpoint.Title}
type="text"
value={accessStore.chatglmUrl}
placeholder={ChatGLM.ExampleEndpoint}
onChange={(e) =>
accessStore.update(
(access) => (access.chatglmUrl = e.currentTarget.value),
)
}
></input>
</ListItem>
<ListItem
title={Locale.Settings.Access.ChatGLM.ApiKey.Title}
subTitle={Locale.Settings.Access.ChatGLM.ApiKey.SubTitle}
>
<PasswordInput
aria-label={Locale.Settings.Access.ChatGLM.ApiKey.Title}
value={accessStore.chatglmApiKey}
type="text"
placeholder={Locale.Settings.Access.ChatGLM.ApiKey.Placeholder}
onChange={(e) => {
accessStore.update(
(access) => (access.chatglmApiKey = e.currentTarget.value),
);
}}
/>
</ListItem>
</>
);
const stabilityConfigComponent = accessStore.provider ===
ServiceProvider.Stability && (
<>
@@ -1146,6 +1288,7 @@ export function Settings() {
}
>
<input
aria-label={Locale.Settings.Access.Stability.Endpoint.Title}
type="text"
value={accessStore.stabilityUrl}
placeholder={Stability.ExampleEndpoint}
@@ -1161,6 +1304,7 @@ export function Settings() {
subTitle={Locale.Settings.Access.Stability.ApiKey.SubTitle}
>
<PasswordInput
aria-label={Locale.Settings.Access.Stability.ApiKey.Title}
value={accessStore.stabilityApiKey}
type="text"
placeholder={Locale.Settings.Access.Stability.ApiKey.Placeholder}
@@ -1184,6 +1328,7 @@ export function Settings() {
}
>
<input
aria-label={Locale.Settings.Access.Iflytek.Endpoint.Title}
type="text"
value={accessStore.iflytekUrl}
placeholder={Iflytek.ExampleEndpoint}
@@ -1199,6 +1344,7 @@ export function Settings() {
subTitle={Locale.Settings.Access.Iflytek.ApiKey.SubTitle}
>
<PasswordInput
aria-label={Locale.Settings.Access.Iflytek.ApiKey.Title}
value={accessStore.iflytekApiKey}
type="text"
placeholder={Locale.Settings.Access.Iflytek.ApiKey.Placeholder}
@@ -1215,6 +1361,7 @@ export function Settings() {
subTitle={Locale.Settings.Access.Iflytek.ApiSecret.SubTitle}
>
<PasswordInput
aria-label={Locale.Settings.Access.Iflytek.ApiSecret.Title}
value={accessStore.iflytekApiSecret}
type="text"
placeholder={Locale.Settings.Access.Iflytek.ApiSecret.Placeholder}
@@ -1244,6 +1391,7 @@ export function Settings() {
<div className="window-action-button"></div>
<div className="window-action-button">
<IconButton
aria={Locale.UI.Close}
icon={<CloseIcon />}
onClick={() => navigate(Path.Home)}
bordered
@@ -1267,6 +1415,8 @@ export function Settings() {
open={showEmojiPicker}
>
<div
aria-label={Locale.Settings.Avatar}
tabIndex={0}
className={styles.avatar}
onClick={() => {
setShowEmojiPicker(!showEmojiPicker);
@@ -1290,9 +1440,17 @@ export function Settings() {
{checkingUpdate ? (
<LoadingIcon />
) : hasNewVersion ? (
<Link href={updateUrl} target="_blank" className="link">
{Locale.Settings.Update.GoToUpdate}
</Link>
clientConfig?.isApp ? (
<IconButton
icon={<ResetIcon></ResetIcon>}
text={Locale.Settings.Update.GoToUpdate}
onClick={() => clientUpdate()}
/>
) : (
<Link href={updateUrl} target="_blank" className="link">
{Locale.Settings.Update.GoToUpdate}
</Link>
)
) : (
<IconButton
icon={<ResetIcon></ResetIcon>}
@@ -1304,6 +1462,7 @@ export function Settings() {
<ListItem title={Locale.Settings.SendKey}>
<Select
aria-label={Locale.Settings.SendKey}
value={config.submitKey}
onChange={(e) => {
updateConfig(
@@ -1322,6 +1481,7 @@ export function Settings() {
<ListItem title={Locale.Settings.Theme}>
<Select
aria-label={Locale.Settings.Theme}
value={config.theme}
onChange={(e) => {
updateConfig(
@@ -1339,6 +1499,7 @@ export function Settings() {
<ListItem title={Locale.Settings.Lang.Name}>
<Select
aria-label={Locale.Settings.Lang.Name}
value={getLang()}
onChange={(e) => {
changeLang(e.target.value as any);
@@ -1357,6 +1518,7 @@ export function Settings() {
subTitle={Locale.Settings.FontSize.SubTitle}
>
<InputRange
aria={Locale.Settings.FontSize.Title}
title={`${config.fontSize ?? 14}px`}
value={config.fontSize}
min="12"
@@ -1376,6 +1538,7 @@ export function Settings() {
subTitle={Locale.Settings.FontFamily.SubTitle}
>
<input
aria-label={Locale.Settings.FontFamily.Title}
type="text"
value={config.fontFamily}
placeholder={Locale.Settings.FontFamily.Placeholder}
@@ -1392,6 +1555,7 @@ export function Settings() {
subTitle={Locale.Settings.AutoGenerateTitle.SubTitle}
>
<input
aria-label={Locale.Settings.AutoGenerateTitle.Title}
type="checkbox"
checked={config.enableAutoGenerateTitle}
onChange={(e) =>
@@ -1408,6 +1572,7 @@ export function Settings() {
subTitle={Locale.Settings.SendPreviewBubble.SubTitle}
>
<input
aria-label={Locale.Settings.SendPreviewBubble.Title}
type="checkbox"
checked={config.sendPreviewBubble}
onChange={(e) =>
@@ -1418,6 +1583,39 @@ export function Settings() {
}
></input>
</ListItem>
<ListItem
title={Locale.Mask.Config.Artifacts.Title}
subTitle={Locale.Mask.Config.Artifacts.SubTitle}
>
<input
aria-label={Locale.Mask.Config.Artifacts.Title}
type="checkbox"
checked={config.enableArtifacts}
onChange={(e) =>
updateConfig(
(config) =>
(config.enableArtifacts = e.currentTarget.checked),
)
}
></input>
</ListItem>
<ListItem
title={Locale.Mask.Config.CodeFold.Title}
subTitle={Locale.Mask.Config.CodeFold.SubTitle}
>
<input
aria-label={Locale.Mask.Config.CodeFold.Title}
type="checkbox"
checked={config.enableCodeFold}
data-testid="enable-code-fold-checkbox"
onChange={(e) =>
updateConfig(
(config) => (config.enableCodeFold = e.currentTarget.checked),
)
}
></input>
</ListItem>
</List>
<SyncItems />
@@ -1428,6 +1626,7 @@ export function Settings() {
subTitle={Locale.Settings.Mask.Splash.SubTitle}
>
<input
aria-label={Locale.Settings.Mask.Splash.Title}
type="checkbox"
checked={!config.dontShowMaskSplashScreen}
onChange={(e) =>
@@ -1445,6 +1644,7 @@ export function Settings() {
subTitle={Locale.Settings.Mask.Builtin.SubTitle}
>
<input
aria-label={Locale.Settings.Mask.Builtin.Title}
type="checkbox"
checked={config.hideBuiltinMasks}
onChange={(e) =>
@@ -1463,6 +1663,7 @@ export function Settings() {
subTitle={Locale.Settings.Prompt.Disable.SubTitle}
>
<input
aria-label={Locale.Settings.Prompt.Disable.Title}
type="checkbox"
checked={config.disablePromptHint}
onChange={(e) =>
@@ -1482,6 +1683,7 @@ export function Settings() {
)}
>
<IconButton
aria={Locale.Settings.Prompt.List + Locale.Settings.Prompt.Edit}
icon={<EditIcon />}
text={Locale.Settings.Prompt.Edit}
onClick={() => setShowPromptModal(true)}
@@ -1490,6 +1692,7 @@ export function Settings() {
</List>
<List id={SlotID.CustomModel}>
{saasStartComponent}
{accessCodeComponent}
{!accessStore.hideUserApiKey && (
@@ -1503,6 +1706,7 @@ export function Settings() {
subTitle={Locale.Settings.Access.Provider.SubTitle}
>
<Select
aria-label={Locale.Settings.Access.Provider.Title}
value={accessStore.provider}
onChange={(e) => {
accessStore.update(
@@ -1531,6 +1735,8 @@ export function Settings() {
{moonshotConfigComponent}
{stabilityConfigComponent}
{lflytekConfigComponent}
{XAIConfigComponent}
{chatglmConfigComponent}
</>
)}
</>
@@ -1565,8 +1771,11 @@ export function Settings() {
<ListItem
title={Locale.Settings.Access.CustomModel.Title}
subTitle={Locale.Settings.Access.CustomModel.SubTitle}
vertical={true}
>
<input
aria-label={Locale.Settings.Access.CustomModel.Title}
style={{ width: "100%", maxWidth: "unset", textAlign: "left" }}
type="text"
value={config.customModels}
placeholder="model1,model2,model3"
@@ -1593,6 +1802,28 @@ export function Settings() {
{shouldShowPromptModal && (
<UserPromptModal onClose={() => setShowPromptModal(false)} />
)}
<List>
<RealtimeConfigList
realtimeConfig={config.realtimeConfig}
updateConfig={(updater) => {
const realtimeConfig = { ...config.realtimeConfig };
updater(realtimeConfig);
config.update(
(config) => (config.realtimeConfig = realtimeConfig),
);
}}
/>
</List>
<List>
<TTSConfigList
ttsConfig={config.ttsConfig}
updateConfig={(updater) => {
const ttsConfig = { ...config.ttsConfig };
updater(ttsConfig);
config.update((config) => (config.ttsConfig = ttsConfig));
}}
/>
</List>
<DangerItems />
</div>

View File

@@ -7,7 +7,6 @@ import SettingsIcon from "../icons/settings.svg";
import GithubIcon from "../icons/github.svg";
import ChatGptIcon from "../icons/chatgpt.svg";
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 DragIcon from "../icons/drag.svg";
@@ -31,6 +30,7 @@ import { Link, useNavigate } from "react-router-dom";
import { isIOS, useMobileScreen } from "../utils";
import dynamic from "next/dynamic";
import { showConfirm, Selector } from "./ui-lib";
import clsx from "clsx";
const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, {
loading: () => null,
@@ -142,9 +142,9 @@ export function SideBarContainer(props: {
const { children, className, onDragStart, shouldNarrow } = props;
return (
<div
className={`${styles.sidebar} ${className} ${
shouldNarrow && styles["narrow-sidebar"]
}`}
className={clsx(styles.sidebar, className, {
[styles["narrow-sidebar"]]: shouldNarrow,
})}
style={{
// #3016 disable transition on ios mobile screen
transition: isMobileScreen && isIOSMobile ? "none" : undefined,
@@ -166,18 +166,24 @@ export function SideBarHeader(props: {
subTitle?: string | React.ReactNode;
logo?: React.ReactNode;
children?: React.ReactNode;
shouldNarrow?: boolean;
}) {
const { title, subTitle, logo, children } = props;
const { title, subTitle, logo, children, shouldNarrow } = props;
return (
<Fragment>
<div className={styles["sidebar-header"]} data-tauri-drag-region>
<div
className={clsx(styles["sidebar-header"], {
[styles["sidebar-header-narrow"]]: shouldNarrow,
})}
data-tauri-drag-region
>
<div className={styles["sidebar-title-container"]}>
<div className={styles["sidebar-title"]} 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 className={clsx(styles["sidebar-logo"], "no-dark")}>{logo}</div>
</div>
{children}
</Fragment>
@@ -228,6 +234,7 @@ export function SideBar(props: { className?: string }) {
title="NextChat"
subTitle="Build your own AI assistant."
logo={<ChatGptIcon />}
shouldNarrow={shouldNarrow}
>
<div className={styles["sidebar-header-bar"]}>
<IconButton
@@ -254,11 +261,6 @@ export function SideBar(props: { className?: string }) {
{showPluginSelector && (
<Selector
items={[
{
title: "👇 Please select the plugin you need to use",
value: "-",
disable: true,
},
...PLUGINS.map((item) => {
return {
title: item.name,
@@ -285,7 +287,7 @@ export function SideBar(props: { className?: string }) {
<SideBarTail
primaryAction={
<>
<div className={styles["sidebar-action"] + " " + styles.mobile}>
<div className={clsx(styles["sidebar-action"], styles.mobile)}>
<IconButton
icon={<DeleteIcon />}
onClick={async () => {
@@ -297,12 +299,20 @@ export function SideBar(props: { className?: string }) {
</div>
<div className={styles["sidebar-action"]}>
<Link to={Path.Settings}>
<IconButton icon={<SettingsIcon />} shadow />
<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 icon={<GithubIcon />} shadow />
<IconButton
aria={Locale.Export.MessageFromChatGPT}
icon={<GithubIcon />}
shadow
/>
</a>
</div>
</>

View File

@@ -0,0 +1,133 @@
import { TTSConfig, TTSConfigValidator } from "../store";
import Locale from "../locales";
import { ListItem, Select } from "./ui-lib";
import {
DEFAULT_TTS_ENGINE,
DEFAULT_TTS_ENGINES,
DEFAULT_TTS_MODELS,
DEFAULT_TTS_VOICES,
} from "../constant";
import { InputRange } from "./input-range";
export function TTSConfigList(props: {
ttsConfig: TTSConfig;
updateConfig: (updater: (config: TTSConfig) => void) => void;
}) {
return (
<>
<ListItem
title={Locale.Settings.TTS.Enable.Title}
subTitle={Locale.Settings.TTS.Enable.SubTitle}
>
<input
type="checkbox"
checked={props.ttsConfig.enable}
onChange={(e) =>
props.updateConfig(
(config) => (config.enable = e.currentTarget.checked),
)
}
></input>
</ListItem>
{/* <ListItem
title={Locale.Settings.TTS.Autoplay.Title}
subTitle={Locale.Settings.TTS.Autoplay.SubTitle}
>
<input
type="checkbox"
checked={props.ttsConfig.autoplay}
onChange={(e) =>
props.updateConfig(
(config) => (config.autoplay = e.currentTarget.checked),
)
}
></input>
</ListItem> */}
<ListItem title={Locale.Settings.TTS.Engine}>
<Select
value={props.ttsConfig.engine}
onChange={(e) => {
props.updateConfig(
(config) =>
(config.engine = TTSConfigValidator.engine(
e.currentTarget.value,
)),
);
}}
>
{DEFAULT_TTS_ENGINES.map((v, i) => (
<option value={v} key={i}>
{v}
</option>
))}
</Select>
</ListItem>
{props.ttsConfig.engine === DEFAULT_TTS_ENGINE && (
<>
<ListItem title={Locale.Settings.TTS.Model}>
<Select
value={props.ttsConfig.model}
onChange={(e) => {
props.updateConfig(
(config) =>
(config.model = TTSConfigValidator.model(
e.currentTarget.value,
)),
);
}}
>
{DEFAULT_TTS_MODELS.map((v, i) => (
<option value={v} key={i}>
{v}
</option>
))}
</Select>
</ListItem>
<ListItem
title={Locale.Settings.TTS.Voice.Title}
subTitle={Locale.Settings.TTS.Voice.SubTitle}
>
<Select
value={props.ttsConfig.voice}
onChange={(e) => {
props.updateConfig(
(config) =>
(config.voice = TTSConfigValidator.voice(
e.currentTarget.value,
)),
);
}}
>
{DEFAULT_TTS_VOICES.map((v, i) => (
<option value={v} key={i}>
{v}
</option>
))}
</Select>
</ListItem>
<ListItem
title={Locale.Settings.TTS.Speed.Title}
subTitle={Locale.Settings.TTS.Speed.SubTitle}
>
<InputRange
aria={Locale.Settings.TTS.Speed.Title}
value={props.ttsConfig.speed?.toFixed(1)}
min="0.3"
max="4.0"
step="0.1"
onChange={(e) => {
props.updateConfig(
(config) =>
(config.speed = TTSConfigValidator.speed(
e.currentTarget.valueAsNumber,
)),
);
}}
></InputRange>
</ListItem>
</>
)}
</>
);
}

View File

@@ -0,0 +1,119 @@
@import "../styles/animation.scss";
.plugin-page {
height: 100%;
display: flex;
flex-direction: column;
.plugin-page-body {
padding: 20px;
overflow-y: auto;
.plugin-filter {
width: 100%;
max-width: 100%;
margin-bottom: 20px;
animation: slide-in ease 0.3s;
height: 40px;
display: flex;
.search-bar {
flex-grow: 1;
max-width: 100%;
min-width: 0;
outline: none;
}
.search-bar:focus {
border: 1px solid var(--primary);
}
.plugin-filter-lang {
height: 100%;
margin-left: 10px;
}
.plugin-create {
height: 100%;
margin-left: 10px;
box-sizing: border-box;
min-width: 80px;
}
}
.plugin-item {
display: flex;
justify-content: space-between;
padding: 20px;
border: var(--border-in-light);
animation: slide-in ease 0.3s;
&:not(:last-child) {
border-bottom: 0;
}
&:first-child {
border-top-left-radius: 10px;
border-top-right-radius: 10px;
}
&:last-child {
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
}
.plugin-header {
display: flex;
align-items: center;
.plugin-icon {
display: flex;
align-items: center;
justify-content: center;
margin-right: 10px;
}
.plugin-title {
.plugin-name {
font-size: 14px;
font-weight: bold;
}
.plugin-info {
font-size: 12px;
}
.plugin-runtime-warning {
font-size: 12px;
color: #f86c6c;
}
}
}
.plugin-actions {
display: flex;
flex-wrap: nowrap;
transition: all ease 0.3s;
justify-content: center;
align-items: center;
}
@media screen and (max-width: 600px) {
display: flex;
flex-direction: column;
padding-bottom: 10px;
border-radius: 10px;
margin-bottom: 20px;
box-shadow: var(--card-shadow);
&:not(:last-child) {
border-bottom: var(--border-in-light);
}
.plugin-actions {
width: 100%;
justify-content: space-between;
padding-top: 10px;
}
}
}
}
}

View File

@@ -62,14 +62,14 @@
}
}
&.vertical{
&.vertical {
flex-direction: column;
align-items: start;
.list-header{
.list-item-title{
.list-header {
.list-item-title {
margin-bottom: 5px;
}
.list-item-sub-title{
.list-item-sub-title {
margin-bottom: 2px;
}
}
@@ -252,6 +252,12 @@
position: relative;
max-width: fit-content;
&.left-align-option {
option {
text-align: left;
}
}
.select-with-icon-select {
height: 100%;
border: var(--border-in-light);
@@ -304,7 +310,7 @@
justify-content: center;
z-index: 999;
.selector-item-disabled{
.selector-item-disabled {
opacity: 0.6;
}
@@ -330,3 +336,4 @@
}
}
}

View File

@@ -23,6 +23,7 @@ import React, {
useRef,
} from "react";
import { IconButton } from "./button";
import clsx from "clsx";
export function Popover(props: {
children: JSX.Element;
@@ -45,13 +46,13 @@ export function Popover(props: {
export function Card(props: { children: JSX.Element[]; className?: string }) {
return (
<div className={styles.card + " " + props.className}>{props.children}</div>
<div className={clsx(styles.card, props.className)}>{props.children}</div>
);
}
export function ListItem(props: {
title: string;
subTitle?: string;
title?: string;
subTitle?: string | JSX.Element;
children?: JSX.Element | JSX.Element[];
icon?: JSX.Element;
className?: string;
@@ -60,11 +61,13 @@ export function ListItem(props: {
}) {
return (
<div
className={
styles["list-item"] +
` ${props.vertical ? styles["vertical"] : ""} ` +
` ${props.className || ""}`
}
className={clsx(
styles["list-item"],
{
[styles["vertical"]]: props.vertical,
},
props.className,
)}
onClick={props.onClick}
>
<div className={styles["list-header"]}>
@@ -135,9 +138,9 @@ export function Modal(props: ModalProps) {
return (
<div
className={
styles["modal-container"] + ` ${isMax && styles["modal-container-max"]}`
}
className={clsx(styles["modal-container"], {
[styles["modal-container-max"]]: isMax,
})}
>
<div className={styles["modal-header"]}>
<div className={styles["modal-title"]}>{props.title}</div>
@@ -260,14 +263,15 @@ export function Input(props: InputProps) {
return (
<textarea
{...props}
className={`${styles["input"]} ${props.className}`}
className={clsx(styles["input"], props.className)}
></textarea>
);
}
export function PasswordInput(props: HTMLProps<HTMLInputElement>) {
export function PasswordInput(
props: HTMLProps<HTMLInputElement> & { aria?: string },
) {
const [visible, setVisible] = useState(false);
function changeVisibility() {
setVisible(!visible);
}
@@ -275,6 +279,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"}
@@ -290,13 +295,23 @@ export function PasswordInput(props: HTMLProps<HTMLInputElement>) {
export function Select(
props: React.DetailedHTMLProps<
React.SelectHTMLAttributes<HTMLSelectElement>,
React.SelectHTMLAttributes<HTMLSelectElement> & {
align?: "left" | "center";
},
HTMLSelectElement
>,
) {
const { className, children, ...otherProps } = props;
const { className, children, align, ...otherProps } = props;
return (
<div className={`${styles["select-with-icon"]} ${className}`}>
<div
className={clsx(
styles["select-with-icon"],
{
[styles["left-align-option"]]: align === "left",
},
className,
)}
>
<select className={styles["select-with-icon-select"]} {...otherProps}>
{children}
</select>
@@ -501,9 +516,9 @@ export function Selector<T>(props: {
const selected = selectedValues.includes(item.value);
return (
<ListItem
className={`${styles["selector-item"]} ${
item.disable && styles["selector-item-disabled"]
}`}
className={clsx(styles["selector-item"], {
[styles["selector-item-disabled"]]: item.disable,
})}
key={i}
title={item.title}
subTitle={item.subTitle}

View File

@@ -0,0 +1 @@
export * from "./voice-print";

View File

@@ -0,0 +1,11 @@
.voice-print {
width: 100%;
height: 60px;
margin: 20px 0;
canvas {
width: 100%;
height: 100%;
filter: brightness(1.2); // 增加整体亮度
}
}

View File

@@ -0,0 +1,180 @@
import { useEffect, useRef, useCallback } from "react";
import styles from "./voice-print.module.scss";
interface VoicePrintProps {
frequencies?: Uint8Array;
isActive?: boolean;
}
export function VoicePrint({ frequencies, isActive }: VoicePrintProps) {
// Canvas引用用于获取绘图上下文
const canvasRef = useRef<HTMLCanvasElement>(null);
// 存储历史频率数据,用于平滑处理
const historyRef = useRef<number[][]>([]);
// 控制保留的历史数据帧数,影响平滑度
const historyLengthRef = useRef(10);
// 存储动画帧ID用于清理
const animationFrameRef = useRef<number>();
/**
* 更新频率历史数据
* 使用FIFO队列维护固定长度的历史记录
*/
const updateHistory = useCallback((freqArray: number[]) => {
historyRef.current.push(freqArray);
if (historyRef.current.length > historyLengthRef.current) {
historyRef.current.shift();
}
}, []);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
/**
* 处理高DPI屏幕显示
* 根据设备像素比例调整canvas实际渲染分辨率
*/
const dpr = window.devicePixelRatio || 1;
canvas.width = canvas.offsetWidth * dpr;
canvas.height = canvas.offsetHeight * dpr;
ctx.scale(dpr, dpr);
/**
* 主要绘制函数
* 使用requestAnimationFrame实现平滑动画
* 包含以下步骤:
* 1. 清空画布
* 2. 更新历史数据
* 3. 计算波形点
* 4. 绘制上下对称的声纹
*/
const draw = () => {
// 清空画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (!frequencies || !isActive) {
historyRef.current = [];
return;
}
const freqArray = Array.from(frequencies);
updateHistory(freqArray);
// 绘制声纹
const points: [number, number][] = [];
const centerY = canvas.height / 2;
const width = canvas.width;
const sliceWidth = width / (frequencies.length - 1);
// 绘制主波形
ctx.beginPath();
ctx.moveTo(0, centerY);
/**
* 声纹绘制算法:
* 1. 使用历史数据平均值实现平滑过渡
* 2. 通过正弦函数添加自然波动
* 3. 使用贝塞尔曲线连接点,使曲线更平滑
* 4. 绘制对称部分形成完整声纹
*/
for (let i = 0; i < frequencies.length; i++) {
const x = i * sliceWidth;
let avgFrequency = frequencies[i];
/**
* 波形平滑处理:
* 1. 收集历史数据中对应位置的频率值
* 2. 计算当前值与历史值的加权平均
* 3. 根据平均值计算实际显示高度
*/
if (historyRef.current.length > 0) {
const historicalValues = historyRef.current.map((h) => h[i] || 0);
avgFrequency =
(avgFrequency + historicalValues.reduce((a, b) => a + b, 0)) /
(historyRef.current.length + 1);
}
/**
* 波形变换:
* 1. 归一化频率值到0-1范围
* 2. 添加时间相关的正弦变换
* 3. 使用贝塞尔曲线平滑连接点
*/
const normalized = avgFrequency / 255.0;
const height = normalized * (canvas.height / 2);
const y = centerY + height * Math.sin(i * 0.2 + Date.now() * 0.002);
points.push([x, y]);
if (i === 0) {
ctx.moveTo(x, y);
} else {
// 使用贝塞尔曲线使波形更平滑
const prevPoint = points[i - 1];
const midX = (prevPoint[0] + x) / 2;
ctx.quadraticCurveTo(
prevPoint[0],
prevPoint[1],
midX,
(prevPoint[1] + y) / 2,
);
}
}
// 绘制对称的下半部分
for (let i = points.length - 1; i >= 0; i--) {
const [x, y] = points[i];
const symmetricY = centerY - (y - centerY);
if (i === points.length - 1) {
ctx.lineTo(x, symmetricY);
} else {
const nextPoint = points[i + 1];
const midX = (nextPoint[0] + x) / 2;
ctx.quadraticCurveTo(
nextPoint[0],
centerY - (nextPoint[1] - centerY),
midX,
centerY - ((nextPoint[1] + y) / 2 - centerY),
);
}
}
ctx.closePath();
/**
* 渐变效果:
* 从左到右应用三色渐变,带透明度
* 使用蓝色系配色提升视觉效果
*/
const gradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
gradient.addColorStop(0, "rgba(100, 180, 255, 0.95)");
gradient.addColorStop(0.5, "rgba(140, 200, 255, 0.9)");
gradient.addColorStop(1, "rgba(180, 220, 255, 0.95)");
ctx.fillStyle = gradient;
ctx.fill();
animationFrameRef.current = requestAnimationFrame(draw);
};
// 启动动画循环
draw();
// 清理函数:在组件卸载时取消动画
return () => {
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
};
}, [frequencies, isActive, updateHistory]);
return (
<div className={styles["voice-print"]}>
<canvas ref={canvasRef} />
</div>
);
}

View File

@@ -40,6 +40,7 @@ export const getBuildConfig = () => {
buildMode,
isApp,
template: process.env.DEFAULT_INPUT_TEMPLATE ?? DEFAULT_INPUT_TEMPLATE,
visionModels: process.env.VISION_MODELS || "",
};
};

View File

@@ -1,5 +1,5 @@
import md5 from "spark-md5";
import { DEFAULT_MODELS } from "../constant";
import { DEFAULT_MODELS, DEFAULT_GA_ID } from "../constant";
declare global {
namespace NodeJS {
@@ -71,6 +71,14 @@ declare global {
IFLYTEK_API_KEY?: string;
IFLYTEK_API_SECRET?: string;
// xai only
XAI_URL?: string;
XAI_API_KEY?: string;
// chatglm only
CHATGLM_URL?: string;
CHATGLM_API_KEY?: string;
// custom template for preprocessing user input
DEFAULT_INPUT_TEMPLATE?: string;
}
@@ -119,10 +127,20 @@ 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("chatgpt-4o") || m.name.startsWith("o1")) &&
!m.name.startsWith("gpt-4o-mini"),
)
.map((m) => "-" + m.name)
.join(",");
if (defaultModel.startsWith("gpt-4")) defaultModel = "";
if (
(defaultModel.startsWith("gpt-4") ||
defaultModel.startsWith("chatgpt-4o") ||
defaultModel.startsWith("o1")) &&
!defaultModel.startsWith("gpt-4o-mini")
)
defaultModel = "";
}
const isStability = !!process.env.STABILITY_API_KEY;
@@ -137,6 +155,8 @@ export const getServerSideConfig = () => {
const isAlibaba = !!process.env.ALIBABA_API_KEY;
const isMoonshot = !!process.env.MOONSHOT_API_KEY;
const isIflytek = !!process.env.IFLYTEK_API_KEY;
const isXAI = !!process.env.XAI_API_KEY;
const isChatGLM = !!process.env.CHATGLM_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);
@@ -145,8 +165,8 @@ export const getServerSideConfig = () => {
// `[Server Config] using ${randomIndex + 1} of ${apiKeys.length} api key`,
// );
const allowedWebDevEndpoints = (
process.env.WHITE_WEBDEV_ENDPOINTS ?? ""
const allowedWebDavEndpoints = (
process.env.WHITE_WEBDAV_ENDPOINTS ?? ""
).split(",");
return {
@@ -199,12 +219,21 @@ export const getServerSideConfig = () => {
iflytekApiKey: process.env.IFLYTEK_API_KEY,
iflytekApiSecret: process.env.IFLYTEK_API_SECRET,
isXAI,
xaiUrl: process.env.XAI_URL,
xaiApiKey: getApiKey(process.env.XAI_API_KEY),
isChatGLM,
chatglmUrl: process.env.CHATGLM_URL,
chatglmApiKey: getApiKey(process.env.CHATGLM_API_KEY),
cloudflareAccountId: process.env.CLOUDFLARE_ACCOUNT_ID,
cloudflareKVNamespaceId: process.env.CLOUDFLARE_KV_NAMESPACE_ID,
cloudflareKVApiKey: getApiKey(process.env.CLOUDFLARE_KV_API_KEY),
cloudflareKVTTL: process.env.CLOUDFLARE_KV_TTL,
gtmId: process.env.GTM_ID,
gaId: process.env.GA_ID || DEFAULT_GA_ID,
needCode: ACCESS_CODES.size > 0,
code: process.env.CODE,
@@ -219,6 +248,6 @@ export const getServerSideConfig = () => {
disableFastLink: !!process.env.DISABLE_FAST_LINK,
customModels,
defaultModel,
allowedWebDevEndpoints,
allowedWebDavEndpoints,
};
};

View File

@@ -1,6 +1,7 @@
export const OWNER = "ChatGPTNextWeb";
export const REPO = "ChatGPT-Next-Web";
export const REPO_URL = `https://github.com/${OWNER}/${REPO}`;
export const PLUGINS_REPO_URL = `https://github.com/${OWNER}/NextChat-Awesome-Plugins`;
export const ISSUE_URL = `https://github.com/${OWNER}/${REPO}/issues`;
export const UPDATE_URL = `${REPO_URL}#keep-updated`;
export const RELEASE_URL = `${REPO_URL}/releases`;
@@ -10,7 +11,6 @@ export const RUNTIME_CONFIG_DOM = "danger-runtime-config";
export const STABILITY_BASE_URL = "https://api.stability.ai";
export const 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";
@@ -28,6 +28,10 @@ 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 XAI_BASE_URL = "https://api.x.ai";
export const CHATGLM_BASE_URL = "https://open.bigmodel.cn";
export const CACHE_URL_PREFIX = "/api/cache";
export const UPLOAD_URL = `${CACHE_URL_PREFIX}/upload`;
@@ -37,10 +41,12 @@ export enum Path {
Settings = "/settings",
NewChat = "/new-chat",
Masks = "/masks",
Plugins = "/plugins",
Auth = "/auth",
Sd = "/sd",
SdNew = "/sd-new",
Artifacts = "/artifacts",
SearchChat = "/search-chat",
}
export enum ApiPath {
@@ -57,6 +63,8 @@ export enum ApiPath {
Iflytek = "/api/iflytek",
Stability = "/api/stability",
Artifacts = "/api/artifacts",
XAI = "/api/xai",
ChatGLM = "/api/chatglm",
}
export enum SlotID {
@@ -69,12 +77,9 @@ export enum FileName {
Prompts = "prompts.json",
}
export enum Plugin {
Artifacts = "artifacts",
}
export enum StoreKey {
Chat = "chat-next-web-store",
Plugin = "chat-next-web-plugin",
Access = "access-control",
Config = "app-config",
Mask = "mask-store",
@@ -112,6 +117,8 @@ export enum ServiceProvider {
Moonshot = "Moonshot",
Stability = "Stability",
Iflytek = "Iflytek",
XAI = "XAI",
ChatGLM = "ChatGLM",
}
// Google API safety settings, see https://ai.google.dev/gemini-api/docs/safety-settings
@@ -134,6 +141,8 @@ export enum ModelProvider {
Hunyuan = "Hunyuan",
Moonshot = "Moonshot",
Iflytek = "Iflytek",
XAI = "XAI",
ChatGLM = "ChatGLM",
}
export const Stability = {
@@ -150,6 +159,7 @@ export const Anthropic = {
export const OpenaiPath = {
ChatPath: "v1/chat/completions",
SpeechPath: "v1/audio/speech",
ImagePath: "v1/images/generations",
UsagePath: "dashboard/billing/usage",
SubsPath: "dashboard/billing/subscription",
@@ -215,6 +225,16 @@ export const Iflytek = {
ChatPath: "v1/chat/completions",
};
export const XAI = {
ExampleEndpoint: XAI_BASE_URL,
ChatPath: "v1/chat/completions",
};
export const ChatGLM = {
ExampleEndpoint: CHATGLM_BASE_URL,
ChatPath: "api/paas/v4/chat/completions",
};
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}}.
@@ -243,15 +263,50 @@ 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-2024-11-20": "2023-10",
"chatgpt-4o-latest": "2023-10",
"gpt-4o-mini": "2023-10",
"gpt-4o-mini-2024-07-18": "2023-10",
"gpt-4-vision-preview": "2023-04",
"o1-mini": "2023-10",
"o1-preview": "2023-10",
// After improvements,
// it's now easier to add "KnowledgeCutOffDate" instead of stupid hardcoding it, as was done previously.
"gemini-pro": "2023-12",
"gemini-pro-vision": "2023-12",
};
export const DEFAULT_TTS_ENGINE = "OpenAI-TTS";
export const DEFAULT_TTS_ENGINES = ["OpenAI-TTS", "Edge-TTS"];
export const DEFAULT_TTS_MODEL = "tts-1";
export const DEFAULT_TTS_VOICE = "alloy";
export const DEFAULT_TTS_MODELS = ["tts-1", "tts-1-hd"];
export const DEFAULT_TTS_VOICES = [
"alloy",
"echo",
"fable",
"onyx",
"nova",
"shimmer",
];
export const VISION_MODEL_REGEXES = [
/vision/,
/gpt-4o/,
/claude-3/,
/gemini-1\.5/,
/gemini-exp/,
/gemini-2\.0/,
/learnlm/,
/qwen-vl/,
/qwen2-vl/,
/gpt-4-turbo(?!.*preview)/, // Matches "gpt-4-turbo" but not "gpt-4-turbo-preview"
/^dall-e-3$/, // Matches exactly "dall-e-3"
];
export const EXCLUDE_VISION_MODEL_REGEXES = [/claude-3-5-haiku-20241022/];
const openaiModels = [
"gpt-3.5-turbo",
"gpt-3.5-turbo-1106",
@@ -264,19 +319,37 @@ const openaiModels = [
"gpt-4-turbo-preview",
"gpt-4o",
"gpt-4o-2024-05-13",
"gpt-4o-2024-08-06",
"gpt-4o-2024-11-20",
"chatgpt-4o-latest",
"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",
"o1-mini",
"o1-preview",
];
const googleModels = [
"gemini-1.0-pro",
"gemini-1.0-pro", // Deprecated on 2/15/2025
"gemini-1.5-pro-latest",
"gemini-1.5-pro",
"gemini-1.5-pro-002",
"gemini-1.5-pro-exp-0827",
"gemini-1.5-flash-latest",
"gemini-pro-vision",
"gemini-1.5-flash-8b-latest",
"gemini-1.5-flash",
"gemini-1.5-flash-8b",
"gemini-1.5-flash-002",
"gemini-1.5-flash-exp-0827",
"learnlm-1.5-pro-experimental",
"gemini-exp-1114",
"gemini-exp-1121",
"gemini-exp-1206",
"gemini-2.0-flash-exp",
"gemini-2.0-flash-thinking-exp-1219",
];
const anthropicModels = [
@@ -285,8 +358,13 @@ const anthropicModels = [
"claude-2.1",
"claude-3-sonnet-20240229",
"claude-3-opus-20240229",
"claude-3-opus-latest",
"claude-3-haiku-20240307",
"claude-3-5-haiku-20241022",
"claude-3-5-haiku-latest",
"claude-3-5-sonnet-20240620",
"claude-3-5-sonnet-20241022",
"claude-3-5-sonnet-latest",
];
const baiduModels = [
@@ -342,6 +420,19 @@ const iflytekModels = [
"4.0Ultra",
];
const xAIModes = ["grok-beta"];
const chatglmModels = [
"glm-4-plus",
"glm-4-0520",
"glm-4",
"glm-4-air",
"glm-4-airx",
"glm-4-long",
"glm-4-flashx",
"glm-4-flash",
];
let seq = 1000; // 内置的模型序号生成器从1000开始
export const DEFAULT_MODELS = [
...openaiModels.map((name) => ({
@@ -454,6 +545,28 @@ export const DEFAULT_MODELS = [
sorted: 10,
},
})),
...xAIModes.map((name) => ({
name,
available: true,
sorted: seq++,
provider: {
id: "xai",
providerName: "XAI",
providerType: "xai",
sorted: 11,
},
})),
...chatglmModels.map((name) => ({
name,
available: true,
sorted: seq++,
provider: {
id: "chatglm",
providerName: "ChatGLM",
providerType: "chatglm",
sorted: 12,
},
})),
] as const;
export const CHAT_PAGE_SIZE = 15;
@@ -472,4 +585,12 @@ export const internalAllowedWebDavEndpoints = [
"https://app.koofr.net/dav/Koofr",
];
export const PLUGINS = [{ name: "Stable Diffusion", path: Path.Sd }];
export const DEFAULT_GA_ID = "G-89WN60ZK2E";
export const PLUGINS = [
{ name: "Plugins", path: Path.Plugins },
{ name: "Stable Diffusion", path: Path.Sd },
{ name: "Search Chat", path: Path.SearchChat },
];
export const SAAS_CHAT_URL = "https://nextchat.dev/chat";
export const SAAS_CHAT_UTM_URL = "https://nextchat.dev/chat?utm=github";

15
app/global.d.ts vendored
View File

@@ -21,10 +21,23 @@ declare interface Window {
writeBinaryFile(path: string, data: Uint8Array): Promise<void>;
writeTextFile(path: string, data: string): Promise<void>;
};
notification:{
notification: {
requestPermission(): Promise<Permission>;
isPermissionGranted(): Promise<boolean>;
sendNotification(options: string | Options): void;
};
updater: {
checkUpdate(): Promise<UpdateResult>;
installUpdate(): Promise<void>;
onUpdaterEvent(
handler: (status: UpdateStatusResult) => void,
): Promise<UnlistenFn>;
};
http: {
fetch<T>(
url: string,
options?: Record<string, unknown>,
): Promise<Response<T>>;
};
};
}

1
app/icons/arrow.svg Normal file
View File

@@ -0,0 +1 @@
<svg class="icon--SJP_d" width="16" height="16" fill="none" viewBox="0 0 16 16" style="min-width: 16px; min-height: 16px;"><g><path data-follow-fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M5.248 14.444a.625.625 0 0 1-.005-.884l5.068-5.12a.625.625 0 0 0 0-.88L5.243 2.44a.625.625 0 1 1 .889-.88l5.067 5.121c.723.73.723 1.907 0 2.638l-5.067 5.12a.625.625 0 0 1-.884.005Z" fill="currentColor"></path></g></svg>

After

Width:  |  Height:  |  Size: 426 B

1
app/icons/fire.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M12.832 21.801c3.126-.626 7.168-2.875 7.168-8.69c0-5.291-3.873-8.815-6.658-10.434c-.619-.36-1.342.113-1.342.828v1.828c0 1.442-.606 4.074-2.29 5.169c-.86.559-1.79-.278-1.894-1.298l-.086-.838c-.1-.974-1.092-1.565-1.87-.971C4.461 8.46 3 10.33 3 13.11C3 20.221 8.289 22 10.933 22q.232 0 .484-.015C10.111 21.874 8 21.064 8 18.444c0-2.05 1.495-3.435 2.631-4.11c.306-.18.663.055.663.41v.59c0 .45.175 1.155.59 1.637c.47.546 1.159-.026 1.214-.744c.018-.226.246-.37.442-.256c.641.375 1.46 1.175 1.46 2.473c0 2.048-1.129 2.99-2.168 3.357"/></svg>

After

Width:  |  Height:  |  Size: 648 B

4
app/icons/hd.svg Normal file
View 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

11
app/icons/headphone.svg Normal file
View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16" height="16" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 28C4 26.8954 4.89543 26 6 26H10V38H6C4.89543 38 4 37.1046 4 36V28Z" fill="none" />
<path d="M38 26H42C43.1046 26 44 26.8954 44 28V36C44 37.1046 43.1046 38 42 38H38V26Z"
fill="none" />
<path
d="M10 36V24C10 16.268 16.268 10 24 10C31.732 10 38 16.268 38 24V36M10 26H6C4.89543 26 4 26.8954 4 28V36C4 37.1046 4.89543 38 6 38H10V26ZM38 26H42C43.1046 26 44 26.8954 44 28V36C44 37.1046 43.1046 38 42 38H38V26Z"
stroke="#333" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" />
<path d="M16 32H20L22 26L26 38L28 32H32" stroke="#333" stroke-width="4" stroke-linecap="round"
stroke-linejoin="round" />
</svg>

After

Width:  |  Height:  |  Size: 808 B

19
app/icons/logo.svg Normal file
View File

@@ -0,0 +1,19 @@
<svg width="38.73" height="42" viewBox="0 0 221 240" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="160.697" y="38.125" width="65.007" height="145.932" rx="32.503" transform="rotate(21.987 160.697 38.125)" fill="url(#logo_svg__a)"></rect>
<path fill-rule="evenodd" clip-rule="evenodd" d="m48.642 79.125-25.92 71.213c-6.139 16.869 2.558 35.52 19.427 41.66 16.868 6.14 35.52-2.558 41.66-19.426L94.23 143.94l-36.658-37.439a32.42 32.42 0 0 1-9.244-23.497c.033-1.326.14-2.62.314-3.879Z" fill="url(#logo_svg__b)"></path>
<path d="M172.578 132.787a32.765 32.765 0 0 1 8.981 24.238c-1.458 28.748-36.622 41.778-56.46 20.92l-67.644-71.122a32.763 32.763 0 0 1-8.981-24.238c1.457-28.748 36.622-41.778 56.46-20.92l67.644 71.122Z" fill="url(#logo_svg__c)" fill-opacity="0.96"></path>
<defs>
<linearGradient id="logo_svg__a" x1="215.063" y1="59.628" x2="160.714" y2="157.96" gradientUnits="userSpaceOnUse">
<stop stop-color="#3EADFE"></stop>
<stop offset="1" stop-color="#2A7AFF"></stop>
</linearGradient>
<linearGradient id="logo_svg__b" x1="105.376" y1="84.416" x2="19.745" y2="131.163" gradientUnits="userSpaceOnUse">
<stop stop-color="#01B3FF"></stop>
<stop offset="1" stop-color="#59ECFA"></stop>
</linearGradient>
<linearGradient id="logo_svg__c" x1="102.734" y1="136.396" x2="192.577" y2="155.859" gradientUnits="userSpaceOnUse">
<stop stop-color="#023BFF" stop-opacity="0.82"></stop>
<stop offset="0.88" stop-color="#2D86FF" stop-opacity="0.76"></stop>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

4
app/icons/palette.svg Normal file
View 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

7
app/icons/power.svg Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M14.5 8C13.8406 8.37652 13.2062 8.79103 12.6 9.24051C11.5625 10.0097 10.6074 10.8814 9.75 11.8402C6.79377 15.1463 5 19.4891 5 24.2455C5 34.6033 13.5066 43 24 43C34.4934 43 43 34.6033 43 24.2455C43 19.4891 41.2062 15.1463 38.25 11.8402C37.3926 10.8814 36.4375 10.0097 35.4 9.24051C34.7938 8.79103 34.1594 8.37652 33.5 8"
stroke="#333" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" />
<path d="M24 4V24" stroke="#333" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" />
</svg>

After

Width:  |  Height:  |  Size: 675 B

View 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 9V37C4 38.1046 4.89543 39 6 39H42C43.1046 39 44 38.1046 44 37V9C44 7.89543 43.1046 7 42 7Z" fill="none" stroke="#000" stroke-width="3" stroke-linejoin="round"/><path d="M12 19H14" stroke="#000" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/><path d="M21 19H23" stroke="#000" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/><path d="M29 19H36" stroke="#000" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/><path d="M12 28H36" stroke="#000" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg>

After

Width:  |  Height:  |  Size: 734 B

1
app/icons/speak-stop.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" width="16" height="16" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4"><path stroke-linecap="round" stroke-linejoin="round" d="M17.25 9.75 19.5 12m0 0 2.25 2.25M19.5 12l2.25-2.25M19.5 12l-2.25 2.25m-10.5-6 4.72-4.72a.75.75 0 0 1 1.28.53v15.88a.75.75 0 0 1-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.009 9.009 0 0 1 2.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75Z"></path></svg>

After

Width:  |  Height:  |  Size: 495 B

1
app/icons/speak.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" width="16" height="16" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4"><path stroke-linecap="round" stroke-linejoin="round" d="M19.114 5.636a9 9 0 010 12.728M16.463 8.288a5.25 5.25 0 010 7.424M6.75 8.25l4.72-4.72a.75.75 0 011.28.53v15.88a.75.75 0 01-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.01 9.01 0 012.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75z"></path></svg>

After

Width:  |  Height:  |  Size: 485 B

13
app/icons/voice-off.svg Normal file
View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M31 24V11C31 7.13401 27.866 4 24 4C20.134 4 17 7.13401 17 11V24C17 27.866 20.134 31 24 31C27.866 31 31 27.866 31 24Z"
stroke="#d0021b" stroke-width="4" stroke-linejoin="round" />
<path
d="M9 23C9 31.2843 15.7157 38 24 38C25.7532 38 27.4361 37.6992 29 37.1465M39 23C39 25.1333 38.5547 27.1626 37.7519 29"
stroke="#d0021b" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" />
<path d="M24 38V44" stroke="#d0021b" stroke-width="4" stroke-linecap="round"
stroke-linejoin="round" />
<path d="M42 42L6 6" stroke="#d0021b" stroke-width="4" stroke-linecap="round"
stroke-linejoin="round" />
</svg>

After

Width:  |  Height:  |  Size: 811 B

16
app/icons/voice-white.svg Normal file
View File

@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" fill="none" viewBox="0 0 20 20">
<defs>
<rect id="path_0" width="20" height="20" x="0" y="0" />
</defs>
<g opacity="1" transform="translate(0 0) rotate(0 8 8)">
<mask id="bg-mask-0" fill="#fff">
<use xlink:href="#path_0" />
</mask>
<g mask="url(#bg-mask-0)">
<path d="M7 4a3 3 0 016 0v6a3 3 0 11-6 0V4z" fill="#333333">
</path>
<path d="M5.5 9.643a.75.75 0 00-1.5 0V10c0 3.06 2.29 5.585 5.25 5.954V17.5h-1.5a.75.75 0 000 1.5h4.5a.75.75 0 000-1.5h-1.5v-1.546A6.001 6.001 0 0016 10v-.357a.75.75 0 00-1.5 0V10a4.5 4.5 0 01-9 0v-.357z" fill="#333333">
</path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 708 B

9
app/icons/voice.svg Normal file
View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="17" y="4" width="14" height="27" rx="7" fill="none" stroke="#333" stroke-width="4"
stroke-linejoin="round" />
<path d="M9 23C9 31.2843 15.7157 38 24 38C32.2843 38 39 31.2843 39 23" stroke="#333"
stroke-width="4" stroke-linecap="round" stroke-linejoin="round" />
<path d="M24 38V44" stroke="#333" stroke-width="4" stroke-linecap="round"
stroke-linejoin="round" />
</svg>

After

Width:  |  Height:  |  Size: 549 B

1
app/icons/zoom.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1.2rem" height="1.2rem" viewBox="0 0 24 24"><g fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></g></svg>

After

Width:  |  Height:  |  Size: 285 B

View File

@@ -6,7 +6,7 @@ import { getClientConfig } from "./config/client";
import type { Metadata, Viewport } from "next";
import { SpeedInsights } from "@vercel/speed-insights/next";
import { getServerSideConfig } from "./config/server";
import { GoogleTagManager } from "@next/third-parties/google";
import { GoogleTagManager, GoogleAnalytics } from "@next/third-parties/google";
const serverConfig = getServerSideConfig();
export const metadata: Metadata = {
@@ -41,7 +41,11 @@ export default function RootLayout({
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<link rel="manifest" href="/site.webmanifest"></link>
<link
rel="manifest"
href="/site.webmanifest"
crossOrigin="use-credentials"
></link>
<script src="/serviceWorkerRegister.js" defer></script>
</head>
<body>
@@ -56,6 +60,11 @@ export default function RootLayout({
<GoogleTagManager gtmId={serverConfig.gtmId} />
</>
)}
{serverConfig?.gaId && (
<>
<GoogleAnalytics gaId={serverConfig.gaId} />
</>
)}
</body>
</html>
);

200
app/lib/audio.ts Normal file
View File

@@ -0,0 +1,200 @@
export class AudioHandler {
private context: AudioContext;
private mergeNode: ChannelMergerNode;
private analyserData: Uint8Array;
public analyser: AnalyserNode;
private workletNode: AudioWorkletNode | null = null;
private stream: MediaStream | null = null;
private source: MediaStreamAudioSourceNode | null = null;
private recordBuffer: Int16Array[] = [];
private readonly sampleRate = 24000;
private nextPlayTime: number = 0;
private isPlaying: boolean = false;
private playbackQueue: AudioBufferSourceNode[] = [];
private playBuffer: Int16Array[] = [];
constructor() {
this.context = new AudioContext({ sampleRate: this.sampleRate });
// using ChannelMergerNode to get merged audio data, and then get analyser data.
this.mergeNode = new ChannelMergerNode(this.context, { numberOfInputs: 2 });
this.analyser = new AnalyserNode(this.context, { fftSize: 256 });
this.analyserData = new Uint8Array(this.analyser.frequencyBinCount);
this.mergeNode.connect(this.analyser);
}
getByteFrequencyData() {
this.analyser.getByteFrequencyData(this.analyserData);
return this.analyserData;
}
async initialize() {
await this.context.audioWorklet.addModule("/audio-processor.js");
}
async startRecording(onChunk: (chunk: Uint8Array) => void) {
try {
if (!this.workletNode) {
await this.initialize();
}
this.stream = await navigator.mediaDevices.getUserMedia({
audio: {
channelCount: 1,
sampleRate: this.sampleRate,
echoCancellation: true,
noiseSuppression: true,
},
});
await this.context.resume();
this.source = this.context.createMediaStreamSource(this.stream);
this.workletNode = new AudioWorkletNode(
this.context,
"audio-recorder-processor",
);
this.workletNode.port.onmessage = (event) => {
if (event.data.eventType === "audio") {
const float32Data = event.data.audioData;
const int16Data = new Int16Array(float32Data.length);
for (let i = 0; i < float32Data.length; i++) {
const s = Math.max(-1, Math.min(1, float32Data[i]));
int16Data[i] = s < 0 ? s * 0x8000 : s * 0x7fff;
}
const uint8Data = new Uint8Array(int16Data.buffer);
onChunk(uint8Data);
// save recordBuffer
// @ts-ignore
this.recordBuffer.push.apply(this.recordBuffer, int16Data);
}
};
this.source.connect(this.workletNode);
this.source.connect(this.mergeNode, 0, 0);
this.workletNode.connect(this.context.destination);
this.workletNode.port.postMessage({ command: "START_RECORDING" });
} catch (error) {
console.error("Error starting recording:", error);
throw error;
}
}
stopRecording() {
if (!this.workletNode || !this.source || !this.stream) {
throw new Error("Recording not started");
}
this.workletNode.port.postMessage({ command: "STOP_RECORDING" });
this.workletNode.disconnect();
this.source.disconnect();
this.stream.getTracks().forEach((track) => track.stop());
}
startStreamingPlayback() {
this.isPlaying = true;
this.nextPlayTime = this.context.currentTime;
}
stopStreamingPlayback() {
this.isPlaying = false;
this.playbackQueue.forEach((source) => source.stop());
this.playbackQueue = [];
this.playBuffer = [];
}
playChunk(chunk: Uint8Array) {
if (!this.isPlaying) return;
const int16Data = new Int16Array(chunk.buffer);
// @ts-ignore
this.playBuffer.push.apply(this.playBuffer, int16Data); // save playBuffer
const float32Data = new Float32Array(int16Data.length);
for (let i = 0; i < int16Data.length; i++) {
float32Data[i] = int16Data[i] / (int16Data[i] < 0 ? 0x8000 : 0x7fff);
}
const audioBuffer = this.context.createBuffer(
1,
float32Data.length,
this.sampleRate,
);
audioBuffer.getChannelData(0).set(float32Data);
const source = this.context.createBufferSource();
source.buffer = audioBuffer;
source.connect(this.context.destination);
source.connect(this.mergeNode, 0, 1);
const chunkDuration = audioBuffer.length / this.sampleRate;
source.start(this.nextPlayTime);
this.playbackQueue.push(source);
source.onended = () => {
const index = this.playbackQueue.indexOf(source);
if (index > -1) {
this.playbackQueue.splice(index, 1);
}
};
this.nextPlayTime += chunkDuration;
if (this.nextPlayTime < this.context.currentTime) {
this.nextPlayTime = this.context.currentTime;
}
}
_saveData(data: Int16Array, bytesPerSample = 16): Blob {
const headerLength = 44;
const numberOfChannels = 1;
const byteLength = data.buffer.byteLength;
const header = new Uint8Array(headerLength);
const view = new DataView(header.buffer);
view.setUint32(0, 1380533830, false); // RIFF identifier 'RIFF'
view.setUint32(4, 36 + byteLength, true); // file length minus RIFF identifier length and file description length
view.setUint32(8, 1463899717, false); // RIFF type 'WAVE'
view.setUint32(12, 1718449184, false); // format chunk identifier 'fmt '
view.setUint32(16, 16, true); // format chunk length
view.setUint16(20, 1, true); // sample format (raw)
view.setUint16(22, numberOfChannels, true); // channel count
view.setUint32(24, this.sampleRate, true); // sample rate
view.setUint32(28, this.sampleRate * 4, true); // byte rate (sample rate * block align)
view.setUint16(32, numberOfChannels * 2, true); // block align (channel count * bytes per sample)
view.setUint16(34, bytesPerSample, true); // bits per sample
view.setUint32(36, 1684108385, false); // data chunk identifier 'data'
view.setUint32(40, byteLength, true); // data chunk length
// using data.buffer, so no need to setUint16 to view.
return new Blob([view, data.buffer], { type: "audio/mpeg" });
}
savePlayFile() {
// @ts-ignore
return this._saveData(new Int16Array(this.playBuffer));
}
saveRecordFile(
audioStartMillis: number | undefined,
audioEndMillis: number | undefined,
) {
const startIndex = audioStartMillis
? Math.floor((audioStartMillis * this.sampleRate) / 1000)
: 0;
const endIndex = audioEndMillis
? Math.floor((audioEndMillis * this.sampleRate) / 1000)
: this.recordBuffer.length;
return this._saveData(
// @ts-ignore
new Int16Array(this.recordBuffer.slice(startIndex, endIndex)),
);
}
async close() {
this.recordBuffer = [];
this.workletNode?.disconnect();
this.source?.disconnect();
this.stream?.getTracks().forEach((track) => track.stop());
await this.context.close();
}
}

View File

@@ -1,115 +1,169 @@
import { SubmitKey } from "../store/config";
import type { PartialLocaleType } from "./index";
import { getClientConfig } from "../config/client";
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
const isApp = !!getClientConfig()?.isApp;
const ar: PartialLocaleType = {
WIP: "قريبًا...",
Error: {
Unauthorized:
"غير مصرح بالوصول، يرجى إدخال رمز الوصول [auth](/#/auth) في صفحة المصادقة.",
Unauthorized: isApp
? `😆 واجهت المحادثة بعض المشكلات، لا داعي للقلق:
\\ 1⃣ إذا كنت ترغب في تجربة دون إعداد، [انقر هنا لبدء المحادثة فورًا 🚀](${SAAS_CHAT_UTM_URL})
\\ 2⃣ إذا كنت تريد استخدام موارد OpenAI الخاصة بك، انقر [هنا](/#/settings) لتعديل الإعدادات ⚙️`
: `😆 واجهت المحادثة بعض المشكلات، لا داعي للقلق:
\ 1⃣ إذا كنت ترغب في تجربة دون إعداد، [انقر هنا لبدء المحادثة فورًا 🚀](${SAAS_CHAT_UTM_URL})
\ 2⃣ إذا كنت تستخدم إصدار النشر الخاص، انقر [هنا](/#/auth) لإدخال مفتاح الوصول 🔑
\ 3⃣ إذا كنت تريد استخدام موارد OpenAI الخاصة بك، انقر [هنا](/#/settings) لتعديل الإعدادات ⚙️
`,
},
Auth: {
Title: "تحتاج إلى رمز الوصول",
Tips: "يرجى إدخال رمز الوصول أدناه",
SubTips: "أو أدخل مفتاح واجهة برمجة تطبيقات OpenAI الخاص بك",
Input: "رمز الوصول",
Title: "تحتاج إلى كلمة مرور",
Tips: "قام المشرف بتفعيل التحقق بكلمة المرور، يرجى إدخال رمز الوصول أدناه",
SubTips: "أو إدخال مفتاح API الخاص بـ OpenAI أو Google",
Input: "أدخل رمز الوصول هنا",
Confirm: "تأكيد",
Later: "لاحقًا",
Later: "في وقت لاحق",
Return: "عودة",
SaasTips: "الإعدادات معقدة، أريد استخدامه على الفور",
TopTips:
"🥳 عرض NextChat AI الأول، افتح الآن OpenAI o1, GPT-4o, Claude-3.5 وأحدث النماذج الكبيرة",
},
ChatItem: {
ChatItemCount: (count: number) => `${count} رسائل`,
ChatItemCount: (count: number) => `${count} محادثة`,
},
Chat: {
SubTitle: (count: number) => ` ${count} رسائل مع ChatGPT`,
SubTitle: (count: number) => `إجمالي ${count} محادثة`,
EditMessage: {
Title: "تحرير سجل الرسائل",
Topic: {
Title: "موضوع الدردشة",
SubTitle: "تغيير موضوع الدردشة الحالي",
},
},
Actions: {
ChatList: "الانتقال إلى قائمة الدردشة",
CompressedHistory: "ملخص ضغط ذاكرة التاريخ",
Export: "تصدير جميع الرسائل كـ Markdown",
ChatList: "عرض قائمة الرسائل",
CompressedHistory: "عرض التاريخ المضغوط",
Export: "تصدير سجل الدردشة",
Copy: "نسخ",
Stop: "توقف",
Stop: "إيقاف",
Retry: "إعادة المحاولة",
Pin: "تثبيت",
PinToastContent: "تم تثبيت 1 محادثة في الإشعارات المسبقة",
PinToastAction: "عرض",
Delete: "حذف",
Edit: "تحرير",
RefreshTitle: "تحديث العنوان",
RefreshToast: "تم إرسال طلب تحديث العنوان",
},
Commands: {
new: "دردشة جديدة",
newm: "إنشاء دردشة من القناع",
next: "الدردشة التالية",
prev: "الدردشة السابقة",
clear: "مسح السياق",
del: "حذف الدردشة",
},
InputActions: {
Stop: "توقف",
ToBottom: "إلى آخر",
Stop: "إيقاف الاستجابة",
ToBottom: "الانتقال إلى الأحدث",
Theme: {
auto: "تلقائي",
light: "نمط فاتح",
dark: "نمط داكن",
auto: "موضوع تلقائي",
light: "الوضع الفاتح",
dark: "الوضع الداكن",
},
Prompt: "الاقتراحات",
Masks: "الأقنعة",
Clear: "مسح السياق",
Settings: "الإعدادات",
Prompt: "الأوامر السريعة",
Masks: "جميع الأقنعة",
Clear: "مسح الدردشة",
Settings: "إعدادات الدردشة",
UploadImage: "تحميل صورة",
},
Rename: "إعادة تسمية الدردشة",
Typing: "كتابة...",
Typing: "يكتب…",
Input: (submitKey: string) => {
var inputHints = ` اضغط على ${submitKey} للإرسال`;
var inputHints = `${submitKey} إرسال`;
if (submitKey === String(SubmitKey.Enter)) {
inputHints += "، Shift + Enter للإنشاء";
inputHints += "، Shift + Enter لإدراج سطر جديد";
}
return inputHints + "، / للبحث في الاقتراحات";
return inputHints + "، / لتفعيل الإكمال التلقائي، : لتفعيل الأوامر";
},
Send: "إرسال",
Config: {
Reset: "إعادة التعيين إلى الإعدادات الافتراضية",
SaveAs: "حفظ كأقنعة",
Reset: "مسح الذاكرة",
SaveAs: "حفظ كقناع",
},
IsContext: "الإشعارات المسبقة",
},
Export: {
Title: "تصدير الرسائل",
Title: "مشاركة سجل الدردشة",
Copy: "نسخ الكل",
Download: نزيل",
MessageFromYou: "رسالة منك",
MessageFromChatGPT: "رسالة من ChatGPT",
Download: حميل الملف",
Share: "مشاركة على ShareGPT",
MessageFromYou: "المستخدم",
MessageFromChatGPT: "ChatGPT",
Format: {
Title: "صيغة التصدير",
SubTitle: "Markdown أو صورة PNG",
Title: "تنسيق التصدير",
SubTitle: "يمكنك تصدير النص كـ Markdown أو صورة PNG",
},
IncludeContext: {
Title: "تضمين السياق",
SubTitle: "تصدير اقتراحات السياق في الأقنعة أم لا",
Title: "تضمين سياق القناع",
SubTitle: "هل تريد عرض سياق القناع في الرسائل",
},
Steps: {
Select: "تحديد",
Select: "اختيار",
Preview: "معاينة",
},
Image: {
Toast: "يتم إنشاء لقطة الشاشة",
Modal: "اضغط مطولاً أو انقر بزر الماوس الأيمن لحفظ الصورة",
},
},
Select: {
Search: "بحث",
Search: "بحث في الرسائل",
All: "تحديد الكل",
Latest: "تحديد أحدث",
Clear: "مسح",
Latest: "أحدث الرسائل",
Clear: "مسح التحديد",
},
Memory: {
Title: "اقتراحات الذاكرة",
EmptyContent: "لا شيء حتى الآن.",
Send: "إرسال الذاكرة",
Copy: "نسخ الذاكرة",
Reset: "إعادة التعيين",
ResetConfirm:
"سيؤدي إعادة التعيين إلى مسح سجل المحادثة الحالي والذاكرة التاريخية. هل أنت متأكد أنك تريد الاستمرار؟",
Title: "ملخص التاريخ",
EmptyContent: "محتوى المحادثة قصير جداً، لا حاجة للتلخيص",
Send: "ضغط تلقائي لسجل الدردشة كـ سياق",
Copy: "نسخ الملخص",
Reset: "[غير مستخدم]",
ResetConfirm: "تأكيد مسح ملخص التاريخ؟",
},
Home: {
NewChat: "دردشة جديدة",
DeleteChat: "هل تريد تأكيد حذف المحادثة المحددة؟",
DeleteToast: "تم حذف الدردشة",
Revert: "التراجع",
DeleteChat: "تأكيد حذف المحادثة المحددة؟",
DeleteToast: "تم حذف المحادثة",
Revert: "تراجع",
},
Settings: {
Title: "الإعدادات",
SubTitle: "جميع الإعدادات",
SubTitle: "جميع خيارات الإعدادات",
Lang: {
Name: "Language", // تنبيه: إذا كنت ترغب في إضافة ترجمة جديدة، يرجى عدم ترجمة هذه القيمة وتركها "Language"
All: "كل اللغات",
Danger: {
Reset: {
Title: "إعادة تعيين جميع الإعدادات",
SubTitle: "إعادة تعيين جميع عناصر الإعدادات إلى القيم الافتراضية",
Action: "إعادة التعيين الآن",
Confirm: "تأكيد إعادة تعيين جميع الإعدادات؟",
},
Clear: {
Title: "مسح جميع البيانات",
SubTitle: "مسح جميع الدردشات وبيانات الإعدادات",
Action: "مسح الآن",
Confirm: "تأكيد مسح جميع الدردشات وبيانات الإعدادات؟",
},
},
Avatar: "الصورة الرمزية",
Lang: {
Name: "Language", // انتبه: إذا كنت ترغب في إضافة ترجمة جديدة، يرجى عدم ترجمة هذه القيمة، اتركها كما هي "Language"
All: "جميع اللغات",
},
Avatar: "الصورة الشخصية",
FontSize: {
Title: "حجم الخط",
SubTitle: "ضبط حجم الخط لمحتوى الدردشة",
SubTitle: "حجم الخط في محتوى الدردشة",
},
FontFamily: {
Title: "خط الدردشة",
@@ -117,163 +171,388 @@ const ar: PartialLocaleType = {
Placeholder: "اسم الخط",
},
InjectSystemPrompts: {
Title: "حقن تلميحات النظام",
Title: "حقن الرسائل النصية النظامية",
SubTitle:
"قم بإضافة تلميحة نظام محاكاة ChatGPT إلى بداية قائمة الرسائل المُطلَبة في كل طلب",
"فرض إضافة رسالة نظامية تحاكي ChatGPT في بداية قائمة الرسائل لكل طلب",
},
InputTemplate: {
Title: "نموذج الإدخال",
SubTitle: "سيتم ملء أحدث رسالة في هذا النموذج",
Title: "معالجة الإدخال من قبل المستخدم",
SubTitle: "سيتم ملء آخر رسالة من المستخدم في هذا القالب",
},
Update: {
Version: (x: string) => ` الإصدار: ${x}`,
IsLatest: "أحدث إصدار",
CheckUpdate: "التحقق من التحديث",
IsChecking: "جارٍ التحقق من التحديث...",
FoundUpdate: (x: string) => ` تم العثور على إصدار جديد: ${x}`,
GoToUpdate: "التحديث",
Version: (x: string) => `الإصدار الحالي: ${x}`,
IsLatest: نت على أحدث إصدار",
CheckUpdate: "التحقق من التحديثات",
IsChecking: "جارٍ التحقق من التحديثات...",
FoundUpdate: (x: string) => `تم العثور على إصدار جديد: ${x}`,
GoToUpdate: "انتقل للتحديث",
},
SendKey: "مفتاح الإرسال",
SendKey: "زر الإرسال",
Theme: "السمة",
TightBorder: "حدود ضيقة",
TightBorder: "وضع بدون حدود",
SendPreviewBubble: {
Title: "عرض معاينة الـ Send",
SubTitle: "معاينة Markdown في فقاعة",
Title: "فقاعة المعاينة",
SubTitle: "معاينة محتوى Markdown في فقاعة المعاينة",
},
AutoGenerateTitle: {
Title: "توليد العنوان تلقائيًا",
SubTitle: "توليد عنوان مناسب بناءً على محتوى الدردشة",
},
Sync: {
CloudState: "بيانات السحابة",
NotSyncYet: "لم يتم التزامن بعد",
Success: "تم التزامن بنجاح",
Fail: "فشل التزامن",
Config: {
Modal: {
Title: "تكوين التزامن السحابي",
Check: "التحقق من التوفر",
},
SyncType: {
Title: "نوع التزامن",
SubTitle: "اختر خادم التزامن المفضل",
},
Proxy: {
Title: "تفعيل الوكيل",
SubTitle: "يجب تفعيل الوكيل عند التزامن عبر المتصفح لتجنب قيود CORS",
},
ProxyUrl: {
Title: "عنوان الوكيل",
SubTitle: "ينطبق فقط على الوكيل المتاح في هذا المشروع",
},
WebDav: {
Endpoint: "عنوان WebDAV",
UserName: "اسم المستخدم",
Password: "كلمة المرور",
},
UpStash: {
Endpoint: "رابط UpStash Redis REST",
UserName: "اسم النسخ الاحتياطي",
Password: "رمز UpStash Redis REST",
},
},
LocalState: "بيانات محلية",
Overview: (overview: any) => {
return `${overview.chat} دردشة، ${overview.message} رسالة، ${overview.prompt} إشعار، ${overview.mask} قناع`;
},
ImportFailed: "فشل الاستيراد",
},
Mask: {
Splash: {
Title: "شاشة تظهر الأقنعة",
SubTitle: "عرض شاشة تظهر الأقنعة قبل بدء الدردشة الجديدة",
Title: "صفحة بدء القناع",
SubTitle: "عرض صفحة بدء القناع عند بدء دردشة جديدة",
},
Builtin: {
Title: "إخفاء الأقنعة المدمجة",
SubTitle: "إخفاء الأقنعة المدمجة في قائمة الأقنعة",
},
},
Prompt: {
Disable: {
Title: "تعطيل الاكتمال التلقائي",
SubTitle: "اكتب / لتشغيل الاكتمال التلقائي",
Title: "تعطيل الإكمال التلقائي للإشعارات",
SubTitle: "استخدم / في بداية مربع النص لتفعيل الإكمال التلقائي",
},
List: "قائمة الاقتراحات",
ListCount: (builtin: number, custom: number) => `
${builtin} مدمجة، ${custom} تم تعريفها من قبل المستخدم`,
Edit: عديل",
List: "قائمة الإشعارات المخصصة",
ListCount: (builtin: number, custom: number) =>
`مدمج ${builtin} إشعار، مخصص ${custom} إشعار`,
Edit: حرير",
Modal: {
Title: "قائمة الاقتراحات",
Add: "إضافة واحدة",
Search: "البحث في الاقتراحات",
Title: "قائمة الإشعارات",
Add: "جديد",
Search: "بحث عن إشعارات",
},
EditModal: {
Title: "تحرير الاقتراح",
Title: "تحرير الإشعارات",
},
},
HistoryCount: {
Title: "عدد الرسائل المرفقة",
SubTitle: "عدد الرسائل المرسلة المرفقة في كل طلب",
Title: "عدد الرسائل التاريخية المرفقة",
SubTitle: "عدد الرسائل التاريخية المرفقة مع كل طلب",
},
CompressThreshold: {
Title: "حد الضغط للتاريخ",
SubTitle: "سيتم الضغط إذا تجاوزت طول الرسائل غير المضغوطة الحد المحدد",
Title: "عتبة ضغط طول الرسائل التاريخية",
SubTitle:
"عندما يتجاوز طول الرسائل التاريخية غير المضغوطة هذه القيمة، سيتم الضغط",
},
Usage: {
Title: "رصيد الحساب",
Title: "التحقق من الرصيد",
SubTitle(used: any, total: any) {
return `تم استخدام $${used} من هذا الشهر، الاشتراك ${total}`;
return `تم استخدام $${used} هذا الشهر، إجمالي الاشتراك $${total}`;
},
IsChecking: "جارٍ التحقق...",
Check: "التحقق",
NoAccess: "أدخل مفتاح API للتحقق من الرصيد",
Check: "إعادة التحقق",
NoAccess: "أدخل مفتاح API أو كلمة مرور للوصول إلى الرصيد",
},
Access: {
SaasStart: {
Title: "استخدام NextChat AI",
Label: "(أفضل حل من حيث التكلفة)",
SubTitle:
"مدعوم رسميًا من NextChat، جاهز للاستخدام بدون إعداد، يدعم أحدث النماذج الكبيرة مثل OpenAI o1 و GPT-4o و Claude-3.5",
ChatNow: "الدردشة الآن",
},
AccessCode: {
Title: "كلمة المرور للوصول",
SubTitle: "قام المشرف بتمكين الوصول المشفر",
Placeholder: "أدخل كلمة المرور للوصول",
},
CustomEndpoint: {
Title: "واجهة مخصصة",
SubTitle: "هل تستخدم خدمة Azure أو OpenAI مخصصة",
},
Provider: {
Title: "موفر الخدمة النموذجية",
SubTitle: "التبديل بين مقدمي الخدمة المختلفين",
},
OpenAI: {
ApiKey: {
Title: "مفتاح API",
SubTitle: "استخدم مفتاح OpenAI مخصص لتجاوز قيود كلمة المرور",
Placeholder: "مفتاح OpenAI API",
},
Endpoint: {
Title: "عنوان الواجهة",
SubTitle: "يجب أن يحتوي على http(s):// بخلاف العنوان الافتراضي",
},
},
Azure: {
ApiKey: {
Title: "مفتاح الواجهة",
SubTitle: "استخدم مفتاح Azure مخصص لتجاوز قيود كلمة المرور",
Placeholder: "مفتاح Azure API",
},
Endpoint: {
Title: "عنوان الواجهة",
SubTitle: "مثال:",
},
ApiVerion: {
Title: "إصدار الواجهة (azure api version)",
SubTitle: "اختر إصدارًا معينًا",
},
},
Anthropic: {
ApiKey: {
Title: "مفتاح الواجهة",
SubTitle: "استخدم مفتاح Anthropic مخصص لتجاوز قيود كلمة المرور",
Placeholder: "مفتاح Anthropic API",
},
Endpoint: {
Title: "عنوان الواجهة",
SubTitle: "مثال:",
},
ApiVerion: {
Title: "إصدار الواجهة (claude api version)",
SubTitle: "اختر إصدار API محدد",
},
},
Google: {
ApiKey: {
Title: "مفتاح API",
SubTitle: "احصل على مفتاح API الخاص بك من Google AI",
Placeholder: "أدخل مفتاح Google AI Studio API",
},
Endpoint: {
Title: "عنوان النهاية",
SubTitle: "مثال:",
},
ApiVersion: {
Title: "إصدار API (مخصص لـ gemini-pro)",
SubTitle: "اختر إصدار API معين",
},
GoogleSafetySettings: {
Title: "مستوى تصفية الأمان من Google",
SubTitle: "تعيين مستوى تصفية المحتوى",
},
},
Baidu: {
ApiKey: {
Title: "مفتاح API",
SubTitle: "استخدم مفتاح Baidu API مخصص",
Placeholder: "مفتاح Baidu API",
},
SecretKey: {
Title: "المفتاح السري",
SubTitle: "استخدم مفتاح Baidu Secret مخصص",
Placeholder: "مفتاح Baidu Secret",
},
Endpoint: {
Title: "عنوان الواجهة",
SubTitle: "لا يدعم التخصيص، انتقل إلى .env للتكوين",
},
},
ByteDance: {
ApiKey: {
Title: "مفتاح الواجهة",
SubTitle: "استخدم مفتاح ByteDance API مخصص",
Placeholder: "مفتاح ByteDance API",
},
Endpoint: {
Title: "عنوان الواجهة",
SubTitle: "مثال:",
},
},
Alibaba: {
ApiKey: {
Title: "مفتاح الواجهة",
SubTitle: "استخدم مفتاح Alibaba Cloud API مخصص",
Placeholder: "مفتاح Alibaba Cloud API",
},
Endpoint: {
Title: "عنوان الواجهة",
SubTitle: "مثال:",
},
},
CustomModel: {
Title: "اسم النموذج المخصص",
SubTitle: "أضف خيارات نموذج مخصص، مفصولة بفواصل إنجليزية",
},
},
Model: "النموذج",
CompressModel: {
Title: "نموذج الضغط",
SubTitle: "النموذج المستخدم لضغط السجل التاريخي",
},
Temperature: {
Title: "الحرارة",
SubTitle: "قيمة أكبر تجعل الإخراج أكثر عشوائية",
Title: "العشوائية (temperature)",
SubTitle: "كلما زادت القيمة، زادت العشوائية في الردود",
},
TopP: {
Title: "عينات النواة (top_p)",
SubTitle: "مشابه للعشوائية ولكن لا تغيره مع العشوائية",
},
MaxTokens: {
Title: "الحد الأقصى للرموز",
SubTitle: "الحد الأقصى لعدد الرموز المدخلة والرموز المُنشأة",
Title: "حد أقصى للرموز لكل رد (max_tokens)",
SubTitle: "أقصى عدد للرموز في تفاعل واحد",
},
PresencePenalty: {
Title: أثير الوجود",
SubTitle: "قيمة أكبر تزيد من احتمالية التحدث عن مواضيع جديدة",
Title: جدد الموضوع (presence_penalty)",
SubTitle: "كلما زادت القيمة، زادت احتمالية التوسع في مواضيع جديدة",
},
FrequencyPenalty: {
Title: "تأثير التكرار",
SubTitle: "قيمة أكبر تقلل من احتمالية تكرار نفس السطر",
Title: "عقوبة التكرار (frequency_penalty)",
SubTitle: "كلما زادت القيمة، زادت احتمالية تقليل تكرار الكلمات",
},
},
Store: {
DefaultTopic: "محادثة جديدة",
BotHello: "مرحبًا! كيف يمكنني مساعدتك اليوم؟",
Error: "حدث خطأ ما، يرجى المحاولة مرة أخرى في وقت لاحق.",
DefaultTopic: "دردشة جديدة",
BotHello: "كيف يمكنني مساعدتك؟",
Error: "حدث خطأ، يرجى المحاولة مرة أخرى لاحقًا",
Prompt: {
History: (content: string) => "هذا ملخص لسجل الدردشة كمراجعة: " + content,
History: (content: string) =>
"هذا ملخص للدردشة السابقة كنقطة انطلاق: " + content,
Topic:
"يرجى إنشاء عنوان يتكون من أربع إلى خمس كلمات يلخص محادثتنا دون أي مقدمة أو ترقيم أو علامات ترقيم أو نقاط أو رموز إضافية. قم بإزالة علامات التنصيص المحيطة.",
"استخدم أربع إلى خمس كلمات لإرجاع ملخص مختصر لهذه الجملة، بدون شرح، بدون علامات ترقيم، بدون كلمات تعبيرية، بدون نص إضافي، بدون تنسيق عريض، إذا لم يكن هناك موضوع، يرجى العودة إلى 'دردشة عامة'",
Summarize:
"قم بتلخيص النقاش بشكل موجز في 200 كلمة أو أقل لاستخدامه كاقتراح للسياق في المستقبل.",
"قم بتلخيص محتوى الدردشة باختصار، لاستخدامه كإشارة سياقية لاحقة، اجعلها في حدود 200 كلمة",
},
},
Copy: {
Success: "تم النسخ إلى الحافظة",
Failed: "فشلت عملية النسخ، يرجى منح الإذن للوصول إلى الحافظة",
Success: "تم الكتابة إلى الحافظة",
Failed: "فشل النسخ، يرجى منح أذونات الحافظة",
},
Download: {
Success: "تم تنزيل المحتوى إلى مجلدك.",
Failed: "فشل التنزيل.",
},
Context: {
Toast: (x: any) => `مع ${x} اقتراحًا ذا سياق`,
Edit: "الاقتراحات السياقية والذاكرة",
Add: "إضافة اقتراح",
Clear: "مسح السياق",
Revert: "التراجع",
Toast: (x: any) => `يحتوي على ${x} إشعارات مخصصة`,
Edit: "إعدادات الدردشة الحالية",
Add: "إضافة دردشة جديدة",
Clear: "تم مسح السياق",
Revert: "استعادة السياق",
},
Plugin: {
Name: "المكوّن الإضافي",
Name: "الإضافات",
},
FineTuned: {
Sysmessage: "أنت مساعد ي",
Sysmessage: "أنت مساعد",
},
Mask: {
Name: "الأقنعة",
SearchChat: {
Name: "بحث",
Page: {
Title: "قالب الاقتراح",
SubTitle: (count: number) => `${count} قوالب الاقتراح`,
Search: "البحث في القوالب",
Create: "إنشاء",
Title: "البحث في سجلات الدردشة",
Search: "أدخل كلمات البحث",
NoResult: "لم يتم العثور على نتائج",
NoData: "لا توجد بيانات",
Loading: "جارٍ التحميل",
SubTitle: (count: number) => `تم العثور على ${count} نتائج`,
},
Item: {
Info: (count: number) => `${count} اقتراحات`,
View: "عرض",
},
},
Mask: {
Name: "القناع",
Page: {
Title: "أقنعة الأدوار المخصصة",
SubTitle: (count: number) => `${count} تعريف لدور مخصص`,
Search: "بحث عن قناع الدور",
Create: "إنشاء جديد",
},
Item: {
Info: (count: number) => `يحتوي على ${count} محادثات مخصصة`,
Chat: "الدردشة",
View: "عرض",
Edit: عديل",
Edit: حرير",
Delete: "حذف",
DeleteConfirm: "تأكيد الحذف؟",
},
EditModal: {
Title: (readonly: boolean) => `
تعديل قالب الاقتراح ${readonly ? "(للقراءة فقط)" : ""}`,
Download: "تنزيل",
Clone: "استنساخ",
Title: (readonly: boolean) =>
`تحرير القناع المخصص ${readonly ? " (للقراءة فقط)" : ""}`,
Download: "تنزيل القناع المخصص",
Clone: "استنساخ القناع",
},
Config: {
Avatar: "صورة الروبوت",
Name: "اسم الروبوت",
Avatar: "صورة الدور",
Name: "اسم الدور",
Sync: {
Title: "استخدام الإعدادات العامة",
SubTitle: "استخدام الإعدادات العامة في هذه الدردشة",
Confirm: "تأكيد الاستبدال بالإعدادات المخصصة بالإعدادات العامة؟",
Title: "استخدام الإعدادات العالمية",
SubTitle: "هل تستخدم الدردشة الحالية الإعدادات العالمية للنموذج",
Confirm:
"ستتم الكتابة فوق الإعدادات المخصصة للدردشة الحالية تلقائيًا، تأكيد تفعيل الإعدادات العالمية؟",
},
HideContext: {
Title: "إخفاء اقتراحات السياق",
SubTitle: "عدم عرض اقتراحات السياق في الدردشة",
Title: "إخفاء المحادثات المخصصة",
SubTitle: "بعد الإخفاء، لن تظهر المحادثات المخصصة في واجهة الدردشة",
},
Share: {
Title: "مشاركة هذا القناع",
SubTitle: "إنشاء رابط مباشر لهذا القناع",
Action: "نسخ الرابط",
},
},
},
NewChat: {
Return: "العودة",
Skip: "ابدأ فقط",
Title: "اختيار قناع",
SubTitle: "دردشة مع الروح وراء القناع",
More: "المزيد",
Skip: "بدء الآن",
NotShow: "عدم العرض مرة أخرى",
ConfirmNoShow: "تأكيد تعطيله؟ يمكنك تمكينه في الإعدادات لاحقًا.",
ConfirmNoShow:
"تأكيد إلغاء العرض؟ بعد الإلغاء، يمكنك إعادة تفعيله في الإعدادات في أي وقت.",
Title: "اختر قناعًا",
SubTitle: "ابدأ الآن وتفاعل مع الأفكار خلف القناع",
More: "عرض الكل",
},
URLCommand: {
Code: "تم الكشف عن رمز وصول في الرابط، هل تريد تعبئته تلقائيًا؟",
Settings: "تم الكشف عن إعدادات مسبقة في الرابط، هل تريد تعبئتها تلقائيًا؟",
},
UI: {
@@ -281,9 +560,16 @@ ${builtin} مدمجة، ${custom} تم تعريفها من قبل المستخد
Cancel: "إلغاء",
Close: "إغلاق",
Create: "إنشاء",
Edit: عديل",
Edit: حرير",
Export: "تصدير",
Import: "استيراد",
Sync: "مزامنة",
Config: "تكوين",
},
Exporter: {
Description: {
Title: "فقط الرسائل بعد مسح السياق سيتم عرضها",
},
Model: "النموذج",
Messages: "الرسائل",
Topic: "الموضوع",

View File

@@ -1,140 +1,169 @@
import { SubmitKey } from "../store/config";
import { PartialLocaleType } from "./index";
import type { PartialLocaleType } from "./index";
import { getClientConfig } from "../config/client";
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
const isApp = !!getClientConfig()?.isApp;
const bn: PartialLocaleType = {
WIP: "শীঘ্রই আসছে...",
Error: {
Unauthorized:
"অননুমোদিত অ্যাক্সেস, অনুগ্রহ করে [অথোরাইজশন](/#/auth) পৃষ্ঠায় অ্যাক্সেস কোড ইনপুট করুন।",
Unauthorized: isApp
? `😆 কথোপকথনে কিছু সমস্যা হয়েছে, চিন্তার কিছু নেই:
\\ 1⃣ যদি আপনি শূন্য কনফিগারেশনে শুরু করতে চান, তাহলে [এখানে ক্লিক করে অবিলম্বে কথোপকথন শুরু করুন 🚀](${SAAS_CHAT_UTM_URL})
\\ 2⃣ যদি আপনি আপনার নিজস্ব OpenAI সম্পদ ব্যবহার করতে চান, তাহলে [এখানে ক্লিক করুন](/#/settings) সেটিংস পরিবর্তন করতে ⚙️`
: `😆 কথোপকথনে কিছু সমস্যা হয়েছে, চিন্তার কিছু নেই:
\ 1⃣ যদি আপনি শূন্য কনফিগারেশনে শুরু করতে চান, তাহলে [এখানে ক্লিক করে অবিলম্বে কথোপকথন শুরু করুন 🚀](${SAAS_CHAT_UTM_URL})
\ 2⃣ যদি আপনি একটি প্রাইভেট ডেপ্লয়মেন্ট সংস্করণ ব্যবহার করেন, তাহলে [এখানে ক্লিক করুন](/#/auth) প্রবেশাধিকার কীগুলি প্রবেশ করতে 🔑
\ 3⃣ যদি আপনি আপনার নিজস্ব OpenAI সম্পদ ব্যবহার করতে চান, তাহলে [এখানে ক্লিক করুন](/#/settings) সেটিংস পরিবর্তন করতে ⚙️
`,
},
Auth: {
Title: "একটি অ্যাক্সেস কোড প্রয়োজন",
Tips: "নীচে অ্যাক্সেস কোড ইনপুট করুন",
SubTips: "অথবা আপনার OpenAI API কী প্রবেশ করন",
Input: "অ্যাক্সেস কোড",
Title: "পাসওয়ার্ড প্রয়োজন",
Tips: "অ্যাডমিন পাসওয়ার্ড প্রমাণীকরণ চালু করেছেন, নিচে অ্যাক্সেস কোড প্রবেশ করুন",
SubTips: "অথবা আপনার OpenAI অথবা Google API কী প্রবেশ করন",
Input: "এখানে অ্যাক্সেস কোড লিখুন",
Confirm: "নিশ্চিত করুন",
Later: "পরে",
Later: "পরে বলুন",
Return: "ফিরে আসা",
SaasTips: "কনফিগারেশন খুব কঠিন, আমি অবিলম্বে ব্যবহার করতে চাই",
TopTips:
"🥳 NextChat AI প্রথম প্রকাশের অফার, এখনই OpenAI o1, GPT-4o, Claude-3.5 এবং সর্বশেষ বড় মডেলগুলি আনলক করুন",
},
ChatItem: {
ChatItemCount: (count: number) => `${count} টি বার্তা`,
ChatItemCount: (count: number) => `${count} টি চ্যাট`,
},
Chat: {
SubTitle: (count: number) => `${count} টি বার্তা`,
SubTitle: (count: number) => `মোট ${count} টি চ্যাট`,
EditMessage: {
Title: "বার্তাগুলি সম্পাদনা করুন",
Topic: {
Title: "চ্যাটের বিষয়",
SubTitle: "বর্তমান চ্যাটের বিষয় পরিবর্তন করুন",
},
},
Actions: {
ChatList: "চ্যাট তালিকায় যান",
CompressedHistory: "সংক্ষিপ্ত ইতিহাস মেমোরি প্রম্পট",
Export: "সমস্ত বার্তা মার্কডাউন হিসাবে রপ্তানি করুন",
Copy: "কপি",
Stop: "বন্ধ করুন",
ChatList: "বার্তা তালিকা দেখুন",
CompressedHistory: "সংকুচিত ইতিহাস দেখুন",
Export: "চ্যাট ইতিহাস রপ্তান করুন",
Copy: "অনুলিপি করুন",
Stop: "থামান",
Retry: "পুনরায় চেষ্টা করুন",
Pin: "পিন করুন",
PinToastContent: "পিন করা হয়েছে ২টি বার্তা প্রম্পটে",
PinToastContent: "1 টি চ্যাট পূর্বনির্ধারিত প্রম্পটে পিন করা হয়েছে",
PinToastAction: "দেখুন",
Delete: "মুছে ফেলুন",
Edit: "সম্পাদন করুন",
Edit: "সম্পাদন করুন",
RefreshTitle: "শিরোনাম রিফ্রেশ করুন",
RefreshToast: "শিরোনাম রিফ্রেশ অনুরোধ পাঠানো হয়েছে",
},
Commands: {
new: "নতুন চ্যাট শুরু করুন",
newm: "মাস্ক সহ নতুন চ্যাট শুরু করুন",
new: "নতুন চ্যাট",
newm: "মাস্ক থেকে নতুন চ্যাট",
next: "পরবর্তী চ্যাট",
prev: "পূর্ববর্তী চ্যাট",
clear: "সংশ্লিষ্টতাবদ্ধকরণ পরিষ্কার করুন",
del: "চ্যাট মুছুন",
clear: "প্রসঙ্গ পরিষ্কার করুন",
del: "চ্যাট মুছে ফেলুন",
},
InputActions: {
Stop: "বন্ধ করুন",
ToBottom: "সর্বশেষতম দিকে",
Stop: "প্রতিক্রিয়া থামান",
ToBottom: "সর্বশেষে স্ক্রোল করুন",
Theme: {
auto: "অটো",
light: "হালকা থিম",
dark: "ডার্ক থিম",
auto: "স্বয়ংক্রিয় থিম",
light: "আলোর মোড",
dark: "অন্ধকার মোড",
},
Prompt: "প্রম্পটগুলিতে",
Masks: "মাস্কগুলি",
Clear: "সংশ্লিষ্টতাবদ্ধকরণ পরিষ্কার করুন",
Settings: "সেটিংস",
Prompt: "সংক্ষিপ্ত নির্দেশনা",
Masks: "সমস্ত মাস্ক",
Clear: "চ্যাট পরিষ্কার করুন",
Settings: "চ্যাট সেটিংস",
UploadImage: "চিত্র আপলোড করুন",
},
Rename: "চ্যাট পুনঃনামকরণ করুন",
Typing: "টাইপিং...",
Rename: "চ্যাট নাম পরিবর্তন করুন",
Typing: "লিখছে…",
Input: (submitKey: string) => {
var inputHints = `${submitKey} to send`;
var inputHints = `${submitKey} পাঠান`;
if (submitKey === String(SubmitKey.Enter)) {
inputHints += ", Shift + Enter to wrap";
inputHints += "Shift + Enter নতুন লাইন";
}
return inputHints + ", / to search prompts, : to use commands";
return inputHints + "/ পূর্ণতা সক্রিয় করুন,: কমান্ড সক্রিয় করুন";
},
Send: "প্রেরণ করুন",
Send: "পাঠান",
Config: {
Reset: "ডিফল্টে রিসেট করুন",
Reset: "মেমরি মুছে ফেলুন",
SaveAs: "মাস্ক হিসাবে সংরক্ষণ করুন",
},
IsContext: "পূর্বনির্ধারিত প্রম্পট",
},
Export: {
Title: "বার্তা রপ্তানিকরণ",
Copy: "সমস্তটি কপি করুন",
Download: "ডাউনলোড করুন",
MessageFromYou: "আপনার বার্তা",
MessageFromChatGPT: "্যাটজিপিটির বার্তা",
Share: "শেয়ার করুন শেয়ারজিপিটি তে",
Title: "চ্যাট ইতিহাস শেয়ার করুন",
Copy: "সবকিছু কপি করুন",
Download: "ফাইল ডাউনলোড করুন",
Share: "ShareGPT তে শেয়ার করুন",
MessageFromYou: "্যবহারকারী",
MessageFromChatGPT: "ChatGPT",
Format: {
Title: "রপ্তানি ফরম্যাট",
SubTitle: "মার্কডাউন বা পিএনজি চিত্র",
Title: "রপ্তানি ফরম্যাট",
SubTitle: "Markdown টেক্সট বা PNG চিত্র রপ্তানি করা যাবে",
},
IncludeContext: {
Title: "মাস্ক অন্তর্ভুক্ত করুন",
SubTitle: "মাস্কগুলি সংরক্ষণ করবেন না কি",
Title: "মাস্ক প্রসঙ্গ অন্তর্ভুক্ত করুন",
SubTitle: "বার্তায় মাস্ক প্রসঙ্গ প্রদর্শন করা হবে কি না",
},
Steps: {
Select: "নির্বাচন করুন",
Preview: "প্রিভিউ করুন",
Preview: "পূর্বরূপ দেখুন",
},
Image: {
Toast: "স্ক্রীনশট তৈরি করা হচ্ছে",
Modal: "ছবি সংরক্ষণ করতে দীর্ঘ প্রেস করুন অথবা রাইট ক্লিক করুন",
},
},
Select: {
Search: "অনুসন্ধান করুন",
All: "সমস্তটি নির্বাচন করুন",
Latest: "সর্বশেষতমটি নির্বাচন করুন",
Clear: "পরিষ্কার করুন",
Search: "বার্তা অনুসন্ধান করুন",
All: "সবকিছু নির্বাচন করুন",
Latest: "সর্বশেষ কিছু",
Clear: "নির্বাচন পরিষ্কার করুন",
},
Memory: {
Title: "মেমোরি প্রম্পট",
EmptyContent: "এখনও কিছুই নেই",
Send: "মেমোরি প্রেরণ করুন",
Copy: "মেমোরি কপি করুন",
Reset: "পুনরায় নিশ্চিত করুন",
ResetConfirm:
"রিসেট করলে বর্তমান চ্যাট ইতিহাস এবং ঐতিহাসিক মেমোরি মুছে যাবে। পুনরায় নির্দিষ্ট করতে চান তা নিশ্চিত করতে চান?",
Title: "ইতিহাস সারাংশ",
EmptyContent: "চ্যাটের বিষয়বস্তু খুব সংক্ষিপ্ত, সারাংশ প্রয়োজন নেই",
Send: "অটোমেটিক চ্যাট ইতিহাস সংকুচিত করুন এবং প্রসঙ্গ হিসেবে পাঠান",
Copy: "সারাংশ কপি করুন",
Reset: "[unused]",
ResetConfirm: "ইতিহাস সারাংশ মুছে ফেলার নিশ্চিত করুন?",
},
Home: {
NewChat: "নতুন চ্যাট",
DeleteChat: "নির্বাচিত সংলাপটি মুছতে নিশ্চিত করুন?",
DeleteToast: "চ্যাটটি মুছেছে",
Revert: "পুনরায়",
DeleteChat: "নির্বাচিত চ্যাট মুছে ফেলার নিশ্চিত করুন?",
DeleteToast: "চ্যাট মুছে ফেলা হয়েছে",
Revert: "পূর্বাবস্থায় ফেরান",
},
Settings: {
Title: "সেটিংস",
SubTitle: "সমস্ত সেটিংস",
SubTitle: "সমস্ত সেটিংস অপশন",
Danger: {
Reset: {
Title: "সমস্ত সেটিংস পুনঃনির্দেশ দিন",
SubTitle: "সকল সেটিংস ডিফল্টে পুনঃনির্দেশ দিতে",
Action: "পুনঃনির্দেশ দিন",
Confirm: "সমস্ত সেটিংস ডিফল্টে পুনঃনির্দেশ করতে নিশ্চিত করতে?",
Title: "সমস্ত সেটিংস পুনরায় সেট করুন",
SubTitle: "সমস্ত সেটিংস বিকল্পগুলিকে ডিফল্ট মানে পুনরায় সেট করুন",
Action: "এখনই পুনরায় সেট করুন",
Confirm: "সমস্ত সেটিংস পুনরায় সেট করার নিশ্চিত করুন?",
},
Clear: {
Title: "সমস্ত তথ্য মুছুন",
SubTitle: "সমস্ত বার্তা এবং সেটিংস মুছুন",
Action: "মুছুন",
Confirm: "সমস্ত বার্তা এবং সেটিংস মুছে ফেলতে নিশ্চিত করতে?",
Title: "সমস্ত তথ্য মুছে ফেলুন",
SubTitle: "সমস্ত চ্যাট এবং সেটিংস ডেটা মুছে ফেলুন",
Action: "এখনই মুছে ফেলুন",
Confirm: "সমস্ত চ্যাট এবং সেটিংস ডেটা মুছে ফেলানোর নিশ্চিত করুন?",
},
},
Lang: {
Name: "বাংলা", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
All: "সমস্ত ভাষা",
},
Avatar: "অতা",
Avatar: "অভিনেতা",
FontSize: {
Title: "ফন্ট সাইজ",
SubTitle: "চ্যাট সামগ্রীর ফন্ট সাইজ সংশোধন করুন",
SubTitle: "চ্যাট কনটেন্টের ফন্ট সাইজ",
},
FontFamily: {
Title: "চ্যাট ফন্ট",
@@ -143,196 +172,417 @@ const bn: PartialLocaleType = {
Placeholder: "ফন্টের নাম",
},
InjectSystemPrompts: {
Title: "حقن تلميحات النظام",
Title: "সিস্টেম-লেভেল প্রম্পট যোগ করুন",
SubTitle:
"قم بإضافة تلميحة نظام محاكاة ChatGPT إلى بداية قائمة الرسائل المُطلَبة في كل طلب",
"প্রত্যেক বার্তায় একটি সিস্টেম প্রম্পট যোগ করুন যা ChatGPT এর অনুকরণ করবে",
},
InputTemplate: {
Title: "ইনপুট টেমপ্লেট",
SubTitle: "নতুনতম বার্তা এই টেমপ্লেটে পূরণ হবে",
Title: "ব্যবহারকারীর ইনপুট প্রিপ্রসেসিং",
SubTitle: "ব্যবহারকারীর সর্বশেষ বার্তা এই টেমপ্লেটে পূরণ করা হবে",
},
Update: {
Version: (x: string) => `Version: ${x}`,
IsLatest: "Latest version",
CheckUpdate: "Check Update",
IsChecking: "Checking update...",
FoundUpdate: (x: string) => `Found new version: ${x}`,
GoToUpdate: "Update",
Version: (x: string) => `বর্তমান সংস্করণ: ${x}`,
IsLatest: "এটি সর্বশেষ সংস্করণ",
CheckUpdate: "আপডেট পরীক্ষা করুন",
IsChecking: "আপডেট পরীক্ষা করা হচ্ছে...",
FoundUpdate: (x: string) => `নতুন সংস্করণ পাওয়া গিয়েছে: ${x}`,
GoToUpdate: "আপডেট করতে যান",
},
SendKey: "প্রেরণ চাবি",
SendKey: "পাঠানোর কী",
Theme: "থিম",
TightBorder: "সঙ্গতি সীমা",
TightBorder: "বর্ডার-বিহীন মোড",
SendPreviewBubble: {
Title: "প্রিভিউ বুবুল প্রেরণ করুন",
SubTitle: "বুবুমার্কডাউন প্রিভিউ করুন",
Title: "প্রিভিউ বুবু",
SubTitle: "প্রিভিউ বুবুMarkdown কনটেন্ট প্রিভিউ করুন",
},
AutoGenerateTitle: {
Title: "স্বয়ংক্রিয় শিরোনাম জেনারেশন",
SubTitle: "চ্যাট কনটেন্টের ভিত্তিতে উপযুক্ত শিরোনাম তৈরি করুন",
},
Sync: {
CloudState: "ক্লাউড ডেটা",
NotSyncYet: "এখনো সিঙ্ক করা হয়নি",
Success: "সিঙ্ক সফল",
Fail: "সিঙ্ক ব্যর্থ",
Config: {
Modal: {
Title: "ক্লাউড সিঙ্ক কনফিগার করুন",
Check: "পরীক্ষা করুন",
},
SyncType: {
Title: "সিঙ্ক টাইপ",
SubTitle: "পছন্দসই সিঙ্ক সার্ভার নির্বাচন করুন",
},
Proxy: {
Title: "প্রক্সি সক্রিয় করুন",
SubTitle:
"ব্রাউজারে সিঙ্ক করার সময়, ক্রস-অরিজিন সীমাবদ্ধতা এড়াতে প্রক্সি সক্রিয় করতে হবে",
},
ProxyUrl: {
Title: "প্রক্সি ঠিকানা",
SubTitle:
"এটি শুধুমাত্র প্রকল্পের সাথে সরবরাহিত ক্রস-অরিজিন প্রক্সির জন্য প্রযোজ্য",
},
WebDav: {
Endpoint: "WebDAV ঠিকানা",
UserName: "ব্যবহারকারীর নাম",
Password: "পাসওয়ার্ড",
},
UpStash: {
Endpoint: "UpStash Redis REST URL",
UserName: "ব্যাকআপ নাম",
Password: "UpStash Redis REST টোকেন",
},
},
LocalState: "স্থানীয় ডেটা",
Overview: (overview: any) => {
return `${overview.chat} বার চ্যাট, ${overview.message} বার্তা, ${overview.prompt} প্রম্পট, ${overview.mask} মাস্ক`;
},
ImportFailed: "আমদানি ব্যর্থ",
},
Mask: {
Splash: {
Title: "মাস্ক স্প্ল্যাশ স্ক্রিন",
SubTitle:
"নতুন চ্যাট শুরু করার আগে মাস্ক স্প্ল্যাশ স্ক্রিন প্রদর্শন করুন",
Title: "মাস্ক লঞ্চ পেজ",
SubTitle: "নতুন চ্যাট শুরু করার সময় মাস্ক লঞ্চ পেজ প্রদর্শন করুন",
},
Builtin: {
Title: "মূলত মাস্ক গোপন করুন",
SubTitle: "মাস্ক তালিকা থেকে মূলত মাস্কগুলি লুকান",
Title: "ইনবিল্ট মাস্ক লুকান",
SubTitle: "সমস্ত মাস্ক তালিকায় ইনবিল্ট মাস্ক লুকান",
},
},
Prompt: {
Disable: {
Title: "অটো-সম্পূর্ণতা নিষ্ক্রিয় করুন",
SubTitle: "অটো-সম্পূর্ণতা চালু করতে / ইনপুট করুন",
Title: "প্রম্পট অটো-কমপ্লিশন নিষ্ক্রিয় করুন",
SubTitle: "ইনপুট বক্সের শুরুতে / টাইপ করলে অটো-কমপ্লিশন সক্রিয় হবে",
},
List: "প্রম্পট তালিকা",
List: "স্বনির্ধারিত প্রম্পট তালিকা",
ListCount: (builtin: number, custom: number) =>
`${builtin} built-in, ${custom} user-defined`,
Edit: "সম্পাদন করুন",
`ইনবিল্ট ${builtin} টি, ব্যবহারকারী সংজ্ঞায়িত ${custom} টি`,
Edit: "সম্পাদন করুন",
Modal: {
Title: "প্রম্পট তালিকা",
Add: "একটি যোগ করুন",
Search: "সন্ধান প্রম্পট",
Add: "নতুন করুন",
Search: "প্রম্পট অনুসন্ধান করুন",
},
EditModal: {
Title: "সম্পাদন করুন প্রম্পট",
Title: "প্রম্পট সম্পাদন করুন",
},
},
HistoryCount: {
Title: "সংযুক্ত বার্তা সংখ্যা",
SubTitle: "প্রতি অনুরোধে প্রেরণ করা গেলে প্রেরণ করা হবে",
Title: "সংযুক্ত ইতিহাস বার্তা সংখ্যা",
SubTitle: "প্রতিটি অনুরোধে সংযুক্ত ইতিহাস বার্তার সংখ্যা",
},
CompressThreshold: {
Title: "ইতিহাস সঙ্কুচিত করার সীমা",
Title: "ইতিহাস বার্তা দৈর্ঘ্য সংকুচিত থ্রেশহোল্ড",
SubTitle:
"নকুল বার্তা দৈর্ঘ্য সীমা অতিক্রান্ত হলে ঐ বার্তাটি সঙ্কুচিত হবে",
"যখন সংকুচিত ইতিহাস বার্তা এই মান ছাড়িয়ে যায়, তখন সংকুচিত করা হবে",
},
Usage: {
Title: "একাউন্ট ব্যালেন্স",
Title: "ব্যালেন্স চেক",
SubTitle(used: any, total: any) {
return `এই মাসে ব্যবহৃত $${used}, সাবস্ক্রিপশন $${total}`;
return `এই মাসে ব্যবহৃত $${used}, সাবস্ক্রিপশন মোট $${total}`;
},
IsChecking: "চেক করা হচ্ছে...",
Check: "চেক",
NoAccess: "ব্যালেন্স চেক করতে অ্যাপি কী ইনপুট করুন",
IsChecking: "পরীক্ষা করা হচ্ছে",
Check: "পুনরায় পরীক্ষা করুন",
NoAccess: "ব্যালেন্স দেখতে API কী অথবা অ্যাক্সেস পাসওয়ার্ড প্রবেশ করুন",
},
Model: "মডেল",
Access: {
SaasStart: {
Title: "NextChat AI ব্যবহার করুন",
Label: "(সেরা মূল্যসাশ্রয়ী সমাধান)",
SubTitle:
"NextChat কর্তৃক অফিসিয়াল রক্ষণাবেক্ষণ, শূন্য কনফিগারেশন ব্যবহার শুরু করুন, OpenAI o1, GPT-4o, Claude-3.5 সহ সর্বশেষ বড় মডেলগুলি সমর্থন করে",
ChatNow: "এখনই চ্যাট করুন",
},
AccessCode: {
Title: "অ্যাক্সেস পাসওয়ার্ড",
SubTitle: "অ্যাডমিন এনক্রিপ্টেড অ্যাক্সেস সক্রিয় করেছেন",
Placeholder: "অ্যাক্সেস পাসওয়ার্ড প্রবেশ করুন",
},
CustomEndpoint: {
Title: "স্বনির্ধারিত ইন্টারফেস",
SubTitle: "স্বনির্ধারিত Azure বা OpenAI সার্ভিস ব্যবহার করবেন কি?",
},
Provider: {
Title: "মডেল পরিষেবা প্রদানকারী",
SubTitle: "বিভিন্ন পরিষেবা প্রদানকারীতে স্যুইচ করুন",
},
OpenAI: {
ApiKey: {
Title: "API কী",
SubTitle:
"পাসওয়ার্ড অ্যাক্সেস সীমাবদ্ধতা এড়াতে স্বনির্ধারিত OpenAI কী ব্যবহার করুন",
Placeholder: "OpenAI API কী",
},
Endpoint: {
Title: "ইন্টারফেস ঠিকানা",
SubTitle: "ডিফল্ট ঠিকানা বাদে, http(s):// অন্তর্ভুক্ত করতে হবে",
},
},
Azure: {
ApiKey: {
Title: "ইন্টারফেস কী",
SubTitle:
"পাসওয়ার্ড অ্যাক্সেস সীমাবদ্ধতা এড়াতে স্বনির্ধারিত Azure কী ব্যবহার করুন",
Placeholder: "Azure API কী",
},
Endpoint: {
Title: "ইন্টারফেস ঠিকানা",
SubTitle: "উদাহরণ:",
},
ApiVerion: {
Title: "ইন্টারফেস সংস্করণ (azure api version)",
SubTitle: "নির্দিষ্ট সংস্করণ নির্বাচন করুন",
},
},
Anthropic: {
ApiKey: {
Title: "ইন্টারফেস কী",
SubTitle:
"পাসওয়ার্ড অ্যাক্সেস সীমাবদ্ধতা এড়াতে স্বনির্ধারিত Anthropic কী ব্যবহার করুন",
Placeholder: "Anthropic API কী",
},
Endpoint: {
Title: "ইন্টারফেস ঠিকানা",
SubTitle: "উদাহরণ:",
},
ApiVerion: {
Title: "ইন্টারফেস সংস্করণ (claude api version)",
SubTitle: "নির্দিষ্ট API সংস্করণ প্রবেশ করুন",
},
},
Google: {
ApiKey: {
Title: "API কী",
SubTitle: "Google AI থেকে আপনার API কী পান",
Placeholder: "আপনার Google AI Studio API কী প্রবেশ করুন",
},
Endpoint: {
Title: "টার্মিনাল ঠিকানা",
SubTitle: "উদাহরণ:",
},
ApiVersion: {
Title: "API সংস্করণ (শুধুমাত্র gemini-pro)",
SubTitle: "একটি নির্দিষ্ট API সংস্করণ নির্বাচন করুন",
},
GoogleSafetySettings: {
Title: "Google সেফটি ফিল্টার স্তর",
SubTitle: "বিষয়বস্তু ফিল্টার স্তর সেট করুন",
},
},
Baidu: {
ApiKey: {
Title: "API কী",
SubTitle: "স্বনির্ধারিত Baidu API কী ব্যবহার করুন",
Placeholder: "Baidu API কী",
},
SecretKey: {
Title: "সিক্রেট কী",
SubTitle: "স্বনির্ধারিত Baidu সিক্রেট কী ব্যবহার করুন",
Placeholder: "Baidu সিক্রেট কী",
},
Endpoint: {
Title: "ইন্টারফেস ঠিকানা",
SubTitle: "স্বনির্ধারিত সমর্থিত নয়, .env কনফিগারেশনে চলে যান",
},
},
ByteDance: {
ApiKey: {
Title: "ইন্টারফেস কী",
SubTitle: "স্বনির্ধারিত ByteDance API কী ব্যবহার করুন",
Placeholder: "ByteDance API কী",
},
Endpoint: {
Title: "ইন্টারফেস ঠিকানা",
SubTitle: "উদাহরণ:",
},
},
Alibaba: {
ApiKey: {
Title: "ইন্টারফেস কী",
SubTitle: "স্বনির্ধারিত আলিবাবা ক্লাউড API কী ব্যবহার করুন",
Placeholder: "Alibaba Cloud API কী",
},
Endpoint: {
Title: "ইন্টারফেস ঠিকানা",
SubTitle: "উদাহরণ:",
},
},
CustomModel: {
Title: "স্বনির্ধারিত মডেল নাম",
SubTitle:
"স্বনির্ধারিত মডেল বিকল্পগুলি যুক্ত করুন, ইংরেজি কমা দ্বারা আলাদা করুন",
},
},
Model: "মডেল (model)",
CompressModel: {
Title: "সংকোচন মডেল",
SubTitle: "ইতিহাস সংকুচিত করার জন্য ব্যবহৃত মডেল",
},
Temperature: {
Title: "তাপমাত্রা",
SubTitle: "আরতি মান বেশি করলে বেশি এলোমেলো আউটপুট হবে",
Title: "যাদুকরিতা (temperature)",
SubTitle: "মান বাড়ালে উত্তর বেশি এলোমেলো হবে",
},
TopP: {
Title: "শীর্ষ পি",
SubTitle: "তাপমাত্রা সঙ্গে এই মান পরিবর্তন করবেন না",
Title: "নিউক্লিয়ার স্যাম্পলিং (top_p)",
SubTitle: "যাদুকরিতা মত, কিন্তু একসাথে পরিবর্তন করবেন না",
},
MaxTokens: {
Title: "সর্বাধিক টোকেন",
SubTitle: "ইনপুট টোকেন এবং উৎপাদিত টোকেনে সর্বাধিক দৈর্ঘ্য",
Title: "একটি উত্তর সীমা (max_tokens)",
SubTitle: "প্রতি ইন্টারঅ্যাকশনে সর্বাধিক টোকেন সংখ্য",
},
PresencePenalty: {
Title: "উপস্থিতির জরিমানা",
SubTitle: "আরতি মান বেশি করলে নতুন বিষয়গুলি সম্ভাব্যতা বাড়াতে পারে",
Title: "বিষয়বস্তু তাজা (presence_penalty)",
SubTitle: "মান বাড়ালে নতুন বিষয়ে প্রসারিত হওয়ার সম্ভাবনা বেশি",
},
FrequencyPenalty: {
Title: "ফ্রিকয়েন্সি জরিমানা",
SubTitle:
"আরতি মান বাড়ালে একই লাইন পুনরায় ব্যাবহার করার সম্ভাবনা হ্রাস পায়",
Title: "ফ্রিকয়েন্সি পেনাল্টি (frequency_penalty)",
SubTitle: "মান বাড়ালে পুনরাবৃত্তি শব্দ কমানোর সম্ভাবনা বেশি",
},
},
Store: {
DefaultTopic: "নতুন সংলাপ",
BotHello: "হ্যালো! আজকে আপনাকে কিভাবে সাহায্য করতে পারি?",
Error: "কিছু নিয়ে ভুল হয়েছে, পরে আবার চেষ্টা করুন",
DefaultTopic: "নতুন চ্যাট",
BotHello: "আপনার জন্য কিছু করতে পারি?",
Error: "একটি ত্রুটি ঘটেছে, পরে আবার চেষ্টা করুন",
Prompt: {
History: (content: string) =>
"এটি চ্যাট ইতিহাসের সংক্ষিপ্ত সংকলনের মতো: " + content,
"এটি পূর্বের চ্যাটের সারাংশ হিসেবে ব্যবহৃত হবে: " + content,
Topic:
"আমাদের সংলাপটির চার থেকে পাঁচ শব্দের একটি শিরোনাম তৈরি করুন যা আমাদের আলাপের সংক্ষিপ্তসার হিসাবে যোগ হবে না, যেমন অভিবৃত্তি, বিন্যাস, উদ্ধৃতি, পূর্বচালক চিহ্ন, পূর্বরোবক্তির যেকোনো চিহ্ন বা অতিরিক্ত পাঠ। মেয়াদশেষ উদ্ধৃতি চেষ্টা করুন",
"চার থেকে পাঁচটি শব্দ ব্যবহার করে এই বাক্যের সংক্ষিপ্ত থিম দিন, ব্যাখ্যা, বিরাম চিহ্ন, ভাষা, অতিরিক্ত টেক্সট বা বোল্ড না ব্যবহার করুন। যদি কোনো থিম না থাকে তবে সরাসরি 'বেকার' বলুন",
Summarize:
"২০০ শব্দের লম্বা হয়ে মুহূর্তে আলোচনা সংক্ষেপের রপ্তানি করুন, যেটি ভবিষ্যতের প্রম্পট হিসবে ব্যবহার করবেন।",
"আলোচনার বিষয়বস্তু সংক্ষিপ্তভাবে সারাংশ করুন, পরবর্তী কনটেক্সট প্রম্পট হিসবে ব্যবহারের জন্য, ২০০ শব্দের মধ্যে সীমাবদ্ধ রাখুন",
},
},
Copy: {
Success: "ক্লিপবোর্ডে কপি করা হয়েছে",
Failed: "কপি ব্যর্থ, অনুমতি প্রদান করার জন্য অনুমতি প্রদান করুন",
Success: "ক্লিপবোর্ডে লেখা হয়েছে",
Failed: "কপি ব্যর্থ হয়েছে, দয়া করে ক্লিপবোর্ড অনুমতি প্রদান করুন",
},
Download: {
Success: "বিষয়বস্তু আপনার ডিরেক্টরিতে ডাউনলোড করা হয়েছে।",
Failed: "ডাউনলোড ব্যর্থ হয়েছে।",
},
Context: {
Toast: (x: any) => `With ${x} contextual prompts`,
Toast: (x: any) => `${x}টি পূর্বনির্ধারিত প্রম্পট অন্তর্ভুক্ত`,
Edit: "বর্তমান চ্যাট সেটিংস",
Add: "একটি প্রম্পট যোগ করুন",
Clear: "সঙ্গতি পরি্কার করুন",
Revert: "পূর্ববর্তী অবস্থানে ফিরে যান",
Add: "একটি নতুন চ্যাট যোগ করুন",
Clear: "কনটেক্সট পরি্কার করা হয়েছে",
Revert: "কনটেক্সট পুনরুদ্ধার করুন",
},
Plugin: {
Name: "প্লাগইন",
},
FineTuned: {
Sysmessage: "আপনি একটি সহকারী যা",
Sysmessage: "আপনি একজন সহকারী",
},
SearchChat: {
Name: "অনুসন্ধান",
Page: {
Title: "চ্যাট রেকর্ড অনুসন্ধান করুন",
Search: "অনুসন্ধান কীওয়ার্ড লিখুন",
NoResult: "কোন ফলাফল পাওয়া যায়নি",
NoData: "কোন তথ্য নেই",
Loading: "লোড হচ্ছে",
SubTitle: (count: number) => `${count} টি ফলাফল পাওয়া গেছে`,
},
Item: {
View: "দেখুন",
},
},
Mask: {
Name: "মাস্ক",
Page: {
Title: "প্রম্পট টেমপ্লেট",
SubTitle: (count: number) => `${count} টি প্রম্পট টেমপ্লেট`,
Search: "টেমপ্লেট অনুসন্ধান করুন",
Create: "তৈরি করুন",
Title: "পূর্বনির্ধারিত চরিত্র মাস্ক",
SubTitle: (count: number) => `${count}টি পূর্বনির্ধারিত চরিত্র সংজ্ঞা`,
Search: "চরিত্র মাস্ক অনুসন্ধান করুন",
Create: "নতুন তৈরি করুন",
},
Item: {
Info: (count: number) => `${count} প্রম্পট`,
Info: (count: number) => `ভিতরে ${count}টি পূর্বনির্ধারিত চ্যাট রয়েছে`,
Chat: "চ্যাট",
View: "দেখুন",
Edit: "সম্পাদন করুন",
Edit: "সম্পাদন করুন",
Delete: "মুছে ফেলুন",
DeleteConfirm: "মুছে ফেলতে নিশ্চিত করুন?",
DeleteConfirm: "মুছে ফেলার জন্য নিশ্চিত করুন?",
},
EditModal: {
Title: (readonly: boolean) =>
`্রম্পট টেমপ্লেট সম্পাদন করুন ${readonly ? "(readonly)" : ""}`,
Download: "ডাউনলোড করুন",
Clone: "ক্লোন করুন",
`ূর্বনির্ধারিত মাস্ক সম্পাদন ${readonly ? "(পঠনযোগ্য)" : ""}`,
Download: "পূর্বনির্ধারিত ডাউনলোড করুন",
Clone: "পূর্বনির্ধারিত ক্লোন করুন",
},
Config: {
Avatar: "বট অবতার",
Name: "বটের নাম",
Avatar: "চরিত্রের চিত্র",
Name: "চরিত্রের নাম",
Sync: {
Title: "গ্লোবাল কনফিগ ব্যবহার করুন",
SubTitle: "এই চ্যাট গ্লোবাল কনফিগ ব্যবহার করুন",
Title: "গ্লোবাল সেটিংস ব্যবহার করুন",
SubTitle: "বর্তমান চ্যাট গ্লোবাল মডেল সেটিংস ব্যবহার করছে কি না",
Confirm:
"গ্লোবাল কনফিগ দ্বারা কাস্টম কনফিগ ওভাররাইড করতে নিশ্চিত করতে?",
"বর্তমান চ্যাটের কাস্টম সেটিংস স্বয়ংক্রিয়ভাবে ওভাররাইট হবে, গ্লোবাল সেটিংস সক্রিয় করতে নিশ্চিত?",
},
HideContext: {
Title: "সংশ্লিষ্টতা প্রম্পটগুলি লুকান",
SubTitle: "চ্যাটে সংশ্লিষ্টতা প্রম্পটগুলি দেখাবেন না",
Title: "পূর্বনির্ধারিত চ্যাট লুকান",
SubTitle:
"লুকানোর পরে পূর্বনির্ধারিত চ্যাট চ্যাট ইন্টারফেসে প্রদর্শিত হবে না",
},
Share: {
Title: "এই মাস্কটি শেয়ার করুন",
SubTitle: "এই মাস্কের একটি লিঙ্ক তৈরি করুন",
Title: "এই মাস্ক শেয়ার করুন",
SubTitle: "এই মাস্কের সরাসরি লিঙ্ক তৈরি করুন",
Action: "লিঙ্ক কপি করুন",
},
},
},
NewChat: {
Return: "ফিরে যান",
Skip: "শুরু করুন",
Title: "মাস্ক নির্বাচন করুন",
SubTitle: "মাস্কের পিছনে আত্মার সঙ্গে চ্যাট করুন",
More: "আরো খুঁজুন",
NotShow: "এখনও দেখাবেন না",
Skip: "ডাইরেক্ট শুরু করুন",
NotShow: "আবার প্রদর্শন করবেন না",
ConfirmNoShow:
"নিষ্ক্রিয় করতে নিশ্চিত করুন? পরে আপনি এটি সেটিংসে সক্ষম করতে পারবে।",
"নিশ্চিত যে নিষ্ক্রিয় করবেন? নিষ্ক্রিয় করার পরে সেটিংসে পুনরায় সক্রিয় করা যাবে।",
Title: "একটি মাস্ক নির্বাচন করুন",
SubTitle: "এখন শুরু করুন, মাস্কের পিছনের চিন্তা প্রতিক্রিয়া করুন",
More: "সব দেখুন",
},
URLCommand: {
Code: "লিঙ্কে অ্যাক্সেস কোড ইতিমধ্যে অন্তর্ভুক্ত রয়েছে, অটো পূরণ করতে চান?",
Settings:
"লিঙ্কে প্রাক-নির্ধারিত সেটিংস অন্তর্ভুক্ত রয়েছে, অটো পূরণ করতে চান?",
},
UI: {
Confirm: "নিশ্চিত করুন",
Cancel: "বাতিল করুন",
Close: "বন্ধ করুন",
Create: "তৈরি করুন",
Edit: "সম্পাদন করুন",
Create: "নতুন তৈরি করুন",
Edit: "সম্পাদন করুন",
Export: "রপ্তানি করুন",
Import: "আমদানি করুন",
Sync: "সিঙ্ক",
Config: "কনফিগারেশন",
},
Exporter: {
Description: {
Title: "শুধুমাত্র কনটেক্সট পরিষ্কার করার পরে বার্তাগুলি প্রদর্শিত হবে",
},
Model: "মডেল",
Messages: "বার্তা",
Topic: "টপিক",
Topic: "থিম",
Time: "সময়",
},
};

View File

@@ -1,5 +1,6 @@
import { getClientConfig } from "../config/client";
import { SubmitKey } from "../store/config";
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
const isApp = !!getClientConfig()?.isApp;
@@ -7,16 +8,26 @@ const cn = {
WIP: "该功能仍在开发中……",
Error: {
Unauthorized: isApp
? "检测到无效 API Key请前往[设置](/#/settings)页检查 API Key 是否配置正确。"
: "访问密码不正确或为空,请前往[登录](/#/auth)页输入正确的访问密码,或者在[设置](/#/settings)页填入你自己的 OpenAI API Key。",
? `😆 对话遇到了一些问题,不用慌:
\\ 1⃣ 想要零配置开箱即用,[点击这里立刻开启对话 🚀](${SAAS_CHAT_UTM_URL})
\\ 2⃣ 如果你想消耗自己的 OpenAI 资源,点击[这里](/#/settings)修改设置 ⚙️`
: `😆 对话遇到了一些问题,不用慌:
\ 1⃣ 想要零配置开箱即用,[点击这里立刻开启对话 🚀](${SAAS_CHAT_UTM_URL})
\ 2⃣ 如果你正在使用私有部署版本,点击[这里](/#/auth)输入访问秘钥 🔑
\ 3⃣ 如果你想消耗自己的 OpenAI 资源,点击[这里](/#/settings)修改设置 ⚙️
`,
},
Auth: {
Return: "返回",
Title: "需要密码",
Tips: "管理员开启了密码验证,请在下方填入访问码",
SubTips: "或者输入你的 OpenAI 或 Google API 密钥",
SubTips: "或者输入你的 OpenAI 或 Google AI 密钥",
Input: "在此处填写访问码",
Confirm: "确认",
Later: "稍后再说",
SaasTips: "配置太麻烦,想要立即使用",
TopTips:
"🥳 NextChat AI 首发优惠,立刻解锁 OpenAI o1, GPT-4o, Claude-3.5 等最新大模型",
},
ChatItem: {
ChatItemCount: (count: number) => `${count} 条对话`,
@@ -42,6 +53,11 @@ const cn = {
PinToastAction: "查看",
Delete: "删除",
Edit: "编辑",
FullScreen: "全屏",
RefreshTitle: "刷新标题",
RefreshToast: "已发送刷新标题请求",
Speech: "朗读",
StopSpeech: "停止",
},
Commands: {
new: "新建聊天",
@@ -49,6 +65,7 @@ const cn = {
next: "下一个聊天",
prev: "上一个聊天",
clear: "清除上下文",
fork: "复制聊天",
del: "删除聊天",
},
InputActions: {
@@ -75,11 +92,21 @@ const cn = {
return inputHints + "/ 触发补全,: 触发命令";
},
Send: "发送",
StartSpeak: "说话",
StopSpeak: "停止",
Config: {
Reset: "清除记忆",
SaveAs: "存为面具",
},
IsContext: "预设提示词",
ShortcutKey: {
Title: "键盘快捷方式",
newChat: "打开新聊天",
focusInput: "聚焦输入框",
copyLastMessage: "复制最后一个回复",
copyLastCode: "复制最后一个代码块",
showShortcutKey: "显示快捷方式",
},
},
Export: {
Title: "分享聊天记录",
@@ -132,6 +159,7 @@ const cn = {
Settings: {
Title: "设置",
SubTitle: "所有设置选项",
ShowPassword: "显示密码",
Danger: {
Reset: {
@@ -177,6 +205,8 @@ const cn = {
IsChecking: "正在检查更新...",
FoundUpdate: (x: string) => `发现新版本:${x}`,
GoToUpdate: "前往更新",
Success: "更新成功!",
Failed: "更新失败",
},
SendKey: "发送键",
Theme: "主题",
@@ -280,6 +310,13 @@ const cn = {
},
Access: {
SaasStart: {
Title: "使用 NextChat AI",
Label: "(性价比最高的方案)",
SubTitle:
"由 NextChat 官方维护, 零配置开箱即用,支持 OpenAI o1, GPT-4o, Claude-3.5 等最新大模型",
ChatNow: "立刻对话",
},
AccessCode: {
Title: "访问密码",
SubTitle: "管理员已开启加密访问",
@@ -343,7 +380,7 @@ const cn = {
ApiKey: {
Title: "API 密钥",
SubTitle: "从 Google AI 获取您的 API 密钥",
Placeholder: "输入您的 Google AI Studio API 密钥",
Placeholder: "Google AI API KEY",
},
Endpoint: {
@@ -425,6 +462,28 @@ const cn = {
SubTitle: "样例:",
},
},
XAI: {
ApiKey: {
Title: "接口密钥",
SubTitle: "使用自定义XAI API Key",
Placeholder: "XAI API Key",
},
Endpoint: {
Title: "接口地址",
SubTitle: "样例:",
},
},
ChatGLM: {
ApiKey: {
Title: "接口密钥",
SubTitle: "使用自定义 ChatGLM API Key",
Placeholder: "ChatGLM API Key",
},
Endpoint: {
Title: "接口地址",
SubTitle: "样例:",
},
},
Stability: {
ApiKey: {
Title: "接口密钥",
@@ -459,6 +518,10 @@ const cn = {
},
Model: "模型 (model)",
CompressModel: {
Title: "对话摘要模型",
SubTitle: "用于压缩历史记录、生成对话标题的模型",
},
Temperature: {
Title: "随机性 (temperature)",
SubTitle: "值越大,回复越随机",
@@ -479,6 +542,59 @@ const cn = {
Title: "频率惩罚度 (frequency_penalty)",
SubTitle: "值越大,越有可能降低重复字词",
},
TTS: {
Enable: {
Title: "启用文本转语音",
SubTitle: "启用文本生成语音服务",
},
Autoplay: {
Title: "启用自动朗读",
SubTitle: "自动生成语音并播放,需先开启文本转语音开关",
},
Model: "模型",
Engine: "转换引擎",
Voice: {
Title: "声音",
SubTitle: "生成语音时使用的声音",
},
Speed: {
Title: "速度",
SubTitle: "生成语音的速度",
},
},
Realtime: {
Enable: {
Title: "实时聊天",
SubTitle: "开启实时聊天功能",
},
Provider: {
Title: "模型服务商",
SubTitle: "切换不同的服务商",
},
Model: {
Title: "模型",
SubTitle: "选择一个模型",
},
ApiKey: {
Title: "API Key",
SubTitle: "API Key",
Placeholder: "API Key",
},
Azure: {
Endpoint: {
Title: "接口地址",
SubTitle: "接口地址",
},
Deployment: {
Title: "部署名称",
SubTitle: "部署名称",
},
},
Temperature: {
Title: "随机性 (temperature)",
SubTitle: "值越大,回复越随机",
},
},
},
Store: {
DefaultTopic: "新的聊天",
@@ -493,8 +609,8 @@ const cn = {
},
},
Copy: {
Success: "已写入剪板",
Failed: "复制失败,请赋予剪板权限",
Success: "已写入剪板",
Failed: "复制失败,请赋予剪板权限",
},
Download: {
Success: "内容已下载到您的目录。",
@@ -507,16 +623,67 @@ const cn = {
Clear: "上下文已清除",
Revert: "恢复上下文",
},
Plugin: {
Name: "插件",
Artifacts: "Artifacts",
},
Discovery: {
Name: "发现",
},
FineTuned: {
Sysmessage: "你是一个助手",
},
SearchChat: {
Name: "搜索",
Page: {
Title: "搜索聊天记录",
Search: "输入搜索关键词",
NoResult: "没有找到结果",
NoData: "没有数据",
Loading: "加载中",
SubTitle: (count: number) => `搜索到 ${count} 条结果`,
},
Item: {
View: "查看",
},
},
Plugin: {
Name: "插件",
Page: {
Title: "插件",
SubTitle: (count: number) => `${count} 个插件`,
Search: "搜索插件",
Create: "新建",
Find: "您可以在Github上找到优秀的插件",
},
Item: {
Info: (count: number) => `${count} 方法`,
View: "查看",
Edit: "编辑",
Delete: "删除",
DeleteConfirm: "确认删除?",
},
Auth: {
None: "不需要授权",
Basic: "Basic",
Bearer: "Bearer",
Custom: "自定义",
CustomHeader: "自定义参数名称",
Token: "Token",
Proxy: "使用代理",
ProxyDescription: "使用代理解决 CORS 错误",
Location: "位置",
LocationHeader: "Header",
LocationQuery: "Query",
LocationBody: "Body",
},
EditModal: {
Title: (readonly: boolean) => `编辑插件 ${readonly ? "(只读)" : ""}`,
Download: "下载",
Auth: "授权方式",
Content: "OpenAPI Schema",
Load: "从网页加载",
Method: "方法",
Error: "格式错误",
},
},
Mask: {
Name: "面具",
Page: {
@@ -551,6 +718,14 @@ const cn = {
Title: "隐藏预设对话",
SubTitle: "隐藏后预设对话不会出现在聊天界面",
},
Artifacts: {
Title: "启用Artifacts",
SubTitle: "启用之后可以直接渲染HTML页面",
},
CodeFold: {
Title: "启用代码折叠",
SubTitle: "启用之后可以自动折叠/展开过长的代码块",
},
Share: {
Title: "分享此面具",
SubTitle: "生成此面具的直达链接",

View File

@@ -1,75 +1,169 @@
import { SubmitKey } from "../store/config";
import type { PartialLocaleType } from "./index";
import { getClientConfig } from "../config/client";
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
const isApp = !!getClientConfig()?.isApp;
const cs: PartialLocaleType = {
WIP: "V přípravě...",
Error: {
Unauthorized:
"Neoprávněný přístup, zadejte přístupový kód na [stránce](/#/auth) nastavení.",
Unauthorized: isApp
? `😆 Rozhovor narazil na nějaké problémy, nebojte se:
\\ 1⃣ Pokud chcete začít bez konfigurace, [klikněte sem pro okamžitý začátek chatu 🚀](${SAAS_CHAT_UTM_URL})
\\ 2⃣ Pokud chcete využít své vlastní zdroje OpenAI, klikněte [sem](/#/settings) a upravte nastavení ⚙️`
: `😆 Rozhovor narazil na nějaké problémy, nebojte se:
\ 1⃣ Pokud chcete začít bez konfigurace, [klikněte sem pro okamžitý začátek chatu 🚀](${SAAS_CHAT_UTM_URL})
\ 2⃣ Pokud používáte verzi soukromého nasazení, klikněte [sem](/#/auth) a zadejte přístupový klíč 🔑
\ 3⃣ Pokud chcete využít své vlastní zdroje OpenAI, klikněte [sem](/#/settings) a upravte nastavení ⚙️
`,
},
Auth: {
Title: "Potřebné heslo",
Tips: "Administrátor povolil ověření heslem, prosím zadejte přístupový kód níže",
SubTips: "nebo zadejte svůj OpenAI nebo Google API klíč",
Input: "Zadejte přístupový kód zde",
Confirm: "Potvrdit",
Later: "Později",
Return: "Návrat",
SaasTips: "Konfigurace je příliš složitá, chci okamžitě začít používat",
TopTips:
"🥳 Uvítací nabídka NextChat AI, okamžitě odemkněte OpenAI o1, GPT-4o, Claude-3.5 a nejnovější velké modely",
},
ChatItem: {
ChatItemCount: (count: number) => `${count} zpráv`,
ChatItemCount: (count: number) => `${count} konverzací`,
},
Chat: {
SubTitle: (count: number) => `${count} zpráv s ChatGPT`,
SubTitle: (count: number) => `Celkem ${count} konverzací`,
EditMessage: {
Title: "Upravit zprávy",
Topic: {
Title: "Téma konverzace",
SubTitle: "Změnit aktuální téma konverzace",
},
},
Actions: {
ChatList: "Přejít na seznam chatů",
CompressedHistory: "Pokyn z komprimované paměti historie",
Export: "Exportovat všechny zprávy jako Markdown",
ChatList: "Zobrazit seznam zpráv",
CompressedHistory: "Zobrazit komprimovanou historii Prompt",
Export: "Exportovat konverzace",
Copy: "Kopírovat",
Stop: "Zastavit",
Retry: "Zopakovat",
Retry: "Zkusit znovu",
Pin: "Připnout",
PinToastContent: "1 konverzace byla připnuta k přednastaveným promptům",
PinToastAction: "Zobrazit",
Delete: "Smazat",
Edit: "Upravit",
RefreshTitle: "Obnovit název",
RefreshToast: "Požadavek na obnovení názvu byl odeslán",
},
Rename: "Přejmenovat chat",
Typing: "Píše...",
Commands: {
new: "Nová konverzace",
newm: "Nová konverzace z masky",
next: "Další konverzace",
prev: "Předchozí konverzace",
clear: "Vymazat kontext",
del: "Smazat konverzaci",
},
InputActions: {
Stop: "Zastavit odpověď",
ToBottom: "Přejít na nejnovější",
Theme: {
auto: "Automatické téma",
light: "Světelný režim",
dark: "Tmavý režim",
},
Prompt: "Rychlé příkazy",
Masks: "Všechny masky",
Clear: "Vymazat konverzaci",
Settings: "Nastavení konverzace",
UploadImage: "Nahrát obrázek",
},
Rename: "Přejmenovat konverzaci",
Typing: "Píše se…",
Input: (submitKey: string) => {
var inputHints = `${submitKey} pro odeslání`;
var inputHints = `${submitKey} odeslat`;
if (submitKey === String(SubmitKey.Enter)) {
inputHints += ", Shift + Enter pro řádkování";
inputHints += "Shift + Enter pro nový řádek";
}
return inputHints + ", / pro vyhledávání pokynů";
return inputHints + "/ pro doplnění, : pro příkaz";
},
Send: "Odeslat",
Config: {
Reset: "Obnovit výchozí",
SaveAs: "Uložit jako Masku",
Reset: "Vymazat paměť",
SaveAs: "Uložit jako masku",
},
IsContext: "Přednastavené prompty",
},
Export: {
Title: "Všechny zprávy",
Title: "Sdílet konverzace",
Copy: "Kopírovat vše",
Download: "Stáhnout",
MessageFromYou: "Zpráva od vás",
MessageFromChatGPT: "Zpráva z ChatGPT",
Download: "Stáhnout soubor",
Share: "Sdílet na ShareGPT",
MessageFromYou: "Uživatel",
MessageFromChatGPT: "ChatGPT",
Format: {
Title: "Formát exportu",
SubTitle: "Lze exportovat jako Markdown text nebo PNG obrázek",
},
IncludeContext: {
Title: "Zahrnout kontext masky",
SubTitle: "Zobrazit kontext masky ve zprávách",
},
Steps: {
Select: "Vybrat",
Preview: "Náhled",
},
Image: {
Toast: "Generování screenshotu",
Modal: "Dlouhým stiskem nebo pravým tlačítkem myši uložte obrázek",
},
},
Select: {
Search: "Hledat zprávy",
All: "Vybrat vše",
Latest: "Několik posledních",
Clear: "Zrušit výběr",
},
Memory: {
Title: "Pokyn z paměti",
EmptyContent: "Zatím nic.",
Send: "Odeslat paměť",
Copy: "Kopírovat paměť",
Reset: "Obnovit relaci",
ResetConfirm:
"Resetováním se vymaže historie aktuálních konverzací i paměť historie pokynů. Opravdu chcete provést obnovu?",
Title: "Historie shrnutí",
EmptyContent: "Obsah konverzace je příliš krátký, není třeba shrnovat",
Send: "Automaticky komprimovat konverzace a odeslat jako kontext",
Copy: "Kopírovat shrnutí",
Reset: "[nepoužívá se]",
ResetConfirm: "Opravdu chcete vymazat historii shrnutí?",
},
Home: {
NewChat: "Nový chat",
DeleteChat: "Potvrzujete smazání vybrané konverzace?",
DeleteToast: "Chat smazán",
Revert: "Zvrátit",
NewChat: "Nová konverzace",
DeleteChat: "Opravdu chcete smazat vybranou konverzaci?",
DeleteToast: "Konverzace byla smazána",
Revert: "Vrátit",
},
Settings: {
Title: "Nastavení",
SubTitle: "Všechna nastavení",
SubTitle: "Všechny možnosti nastavení",
Danger: {
Reset: {
Title: "Obnovit všechna nastavení",
SubTitle: "Obnovit všechny nastavení na výchozí hodnoty",
Action: "Okamžitě obnovit",
Confirm: "Opravdu chcete obnovit všechna nastavení?",
},
Clear: {
Title: "Smazat všechna data",
SubTitle: "Smazat všechny chaty a nastavení",
Action: "Okamžitě smazat",
Confirm: "Opravdu chcete smazat všechny chaty a nastavení?",
},
},
Lang: {
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
Name: "Language", // POZOR: pokud chcete přidat nový překlad, prosím, nechte tuto hodnotu jako `Language`
All: "Všechny jazyky",
},
Avatar: "Avatar",
Avatar: "Profilový obrázek",
FontSize: {
Title: "Velikost písma",
SubTitle: "Nastavení velikosti písma obsahu chatu",
SubTitle: "Velikost písma pro obsah chatu",
},
FontFamily: {
Title: "Chatové Písmo",
@@ -78,162 +172,415 @@ const cs: PartialLocaleType = {
Placeholder: "Název Písma",
},
InjectSystemPrompts: {
Title: "Vložit systémové prompty",
Title: "Vložit systémové výzvy",
SubTitle:
"Vynutit přidání simulovaného systémového promptu ChatGPT na začátek seznamu zpráv každého požadavku",
"Automaticky přidat systémovou výzvu simulující ChatGPT na začátek seznamu zpráv pro každý požadavek",
},
InputTemplate: {
Title: "Předzpracování uživatelského vstupu",
SubTitle: "Nejnovější zpráva uživatele bude vyplněna do této šablony",
},
Update: {
Version: (x: string) => `Verze: ${x}`,
IsLatest: "Aktuální verze",
Version: (x: string) => `Aktuální verze: ${x}`,
IsLatest: "Jste na nejnovější verzi",
CheckUpdate: "Zkontrolovat aktualizace",
IsChecking: "Kontrola aktualizace...",
IsChecking: "Kontrola aktualizací...",
FoundUpdate: (x: string) => `Nalezena nová verze: ${x}`,
GoToUpdate: "Aktualizovat",
GoToUpdate: "Přejít na aktualizaci",
},
SendKey: "Odeslat klíč",
SendKey: "Klávesa pro odeslání",
Theme: "Téma",
TightBorder: "Těsné ohraničení",
TightBorder: "Režim bez okra",
SendPreviewBubble: {
Title: "Odesílat chatovací bublinu s náhledem",
SubTitle: "Zobrazit v náhledu bubliny",
Title: "Náhledová bublina",
SubTitle: "Náhled Markdown obsahu v náhledové bublině",
},
AutoGenerateTitle: {
Title: "Automatické generování názvu",
SubTitle: "Generovat vhodný název na základě obsahu konverzace",
},
Sync: {
CloudState: "Data na cloudu",
NotSyncYet: "Ještě nebylo synchronizováno",
Success: "Synchronizace úspěšná",
Fail: "Synchronizace selhala",
Config: {
Modal: {
Title: "Nastavení cloudové synchronizace",
Check: "Zkontrolovat dostupnost",
},
SyncType: {
Title: "Typ synchronizace",
SubTitle: "Vyberte oblíbený synchronizační server",
},
Proxy: {
Title: "Povolit proxy",
SubTitle:
"Při synchronizaci v prohlížeči musí být proxy povolena, aby se předešlo problémům s CORS",
},
ProxyUrl: {
Title: "Adresa proxy",
SubTitle: "Pouze pro interní proxy",
},
WebDav: {
Endpoint: "WebDAV adresa",
UserName: "Uživatelské jméno",
Password: "Heslo",
},
UpStash: {
Endpoint: "UpStash Redis REST URL",
UserName: "Název zálohy",
Password: "UpStash Redis REST Token",
},
},
LocalState: "Lokální data",
Overview: (overview: any) => {
return `${overview.chat} konverzací, ${overview.message} zpráv, ${overview.prompt} promptů, ${overview.mask} masek`;
},
ImportFailed: "Import selhal",
},
Mask: {
Splash: {
Title: "Úvodní obrazovka Masek",
SubTitle: "Před zahájením nového chatu zobrazte úvodní obrazovku Masek",
Title: "Úvodní stránka masky",
SubTitle: "Při zahájení nové konverzace zobrazit úvodní stránku masky",
},
Builtin: {
Title: "Skrýt vestavěné masky",
SubTitle: "Skrýt vestavěné masky v seznamu všech masek",
},
},
Prompt: {
Disable: {
Title: "Deaktivovat automatické dokončování",
SubTitle: "Zadejte / pro spuštění automatického dokončování",
Title: "Zakázat automatické doplňování promptů",
SubTitle:
"Automatické doplňování se aktivuje zadáním / na začátku textového pole",
},
List: "Seznam pokynů",
List: "Seznam vlastních promptů",
ListCount: (builtin: number, custom: number) =>
`${builtin} vestavěných, ${custom} uživatelských`,
`Vestavěné ${builtin} položek, uživatelsky definované ${custom} položek`,
Edit: "Upravit",
Modal: {
Title: "Seznam pokynů",
Add: "Přidat pokyn",
Search: "Hledat pokyny",
Title: "Seznam promptů",
Add: "Nový",
Search: "Hledat prompty",
},
EditModal: {
Title: "Editovat pokyn",
Title: "Upravit prompt",
},
},
HistoryCount: {
Title: "Počet připojených zpráv",
SubTitle: "Počet odeslaných připojených zpráv na žádost",
Title: "Počet historických zpráv",
SubTitle: "Počet historických zpráv zahrnutých v každém požadavku",
},
CompressThreshold: {
Title: "Práh pro kompresi historie",
Title: "Prahová hodnota komprese historických zpráv",
SubTitle:
"Komprese proběhne, pokud délka nekomprimovaných zpráv přesáhne tuto hodnotu",
"Když nekomprimované historické zprávy překročí tuto hodnotu, dojde ke kompresi",
},
Usage: {
Title: "Stav účtu",
Title: "Kontrola zůstatku",
SubTitle(used: any, total: any) {
return `Použito tento měsíc $${used}, předplaceno $${total}`;
return `Tento měsíc použito $${used}, celkový předplatný objem $${total}`;
},
IsChecking: "Kontroluji...",
Check: "Zkontrolovat",
NoAccess: "Pro kontrolu zůstatku zadejte klíč API",
IsChecking: "Probíhá kontrola…",
Check: "Znovu zkontrolovat",
NoAccess: "Zadejte API Key nebo přístupové heslo pro zobrazení zůstatku",
},
Model: "Model",
Access: {
SaasStart: {
Title: "Použití NextChat AI",
Label: "(Nejlepší nákladově efektivní řešení)",
SubTitle:
"Oficiálně udržováno NextChat, připraveno k použití bez konfigurace, podporuje nejnovější velké modely jako OpenAI o1, GPT-4o, Claude-3.5",
ChatNow: "Začněte chatovat nyní",
},
AccessCode: {
Title: "Přístupový kód",
SubTitle: "Administrátor aktivoval šifrovaný přístup",
Placeholder: "Zadejte přístupový kód",
},
CustomEndpoint: {
Title: "Vlastní rozhraní",
SubTitle: "Použít vlastní Azure nebo OpenAI službu",
},
Provider: {
Title: "Poskytovatel modelu",
SubTitle: "Přepnout mezi různými poskytovateli",
},
OpenAI: {
ApiKey: {
Title: "API Key",
SubTitle:
"Použijte vlastní OpenAI Key k obejití přístupového omezení",
Placeholder: "OpenAI API Key",
},
Endpoint: {
Title: "Adresa rozhraní",
SubTitle: "Kromě výchozí adresy musí obsahovat http(s)://",
},
},
Azure: {
ApiKey: {
Title: "Rozhraní klíč",
SubTitle: "Použijte vlastní Azure Key k obejití přístupového omezení",
Placeholder: "Azure API Key",
},
Endpoint: {
Title: "Adresa rozhraní",
SubTitle: "Příklad:",
},
ApiVerion: {
Title: "Verze rozhraní (azure api version)",
SubTitle: "Vyberte konkrétní verzi",
},
},
Anthropic: {
ApiKey: {
Title: "Rozhraní klíč",
SubTitle:
"Použijte vlastní Anthropic Key k obejití přístupového omezení",
Placeholder: "Anthropic API Key",
},
Endpoint: {
Title: "Adresa rozhraní",
SubTitle: "Příklad:",
},
ApiVerion: {
Title: "Verze rozhraní (claude api version)",
SubTitle: "Vyberte konkrétní verzi API",
},
},
Google: {
ApiKey: {
Title: "API klíč",
SubTitle: "Získejte svůj API klíč od Google AI",
Placeholder: "Zadejte svůj Google AI Studio API klíč",
},
Endpoint: {
Title: "Konečná adresa",
SubTitle: "Příklad:",
},
ApiVersion: {
Title: "Verze API (pouze pro gemini-pro)",
SubTitle: "Vyberte konkrétní verzi API",
},
GoogleSafetySettings: {
Title: "Úroveň bezpečnostního filtrování Google",
SubTitle: "Nastavit úroveň filtrování obsahu",
},
},
Baidu: {
ApiKey: {
Title: "API Key",
SubTitle: "Použijte vlastní Baidu API Key",
Placeholder: "Baidu API Key",
},
SecretKey: {
Title: "Secret Key",
SubTitle: "Použijte vlastní Baidu Secret Key",
Placeholder: "Baidu Secret Key",
},
Endpoint: {
Title: "Adresa rozhraní",
SubTitle:
"Nepodporuje vlastní nastavení, přejděte na .env konfiguraci",
},
},
ByteDance: {
ApiKey: {
Title: "Rozhraní klíč",
SubTitle: "Použijte vlastní ByteDance API Key",
Placeholder: "ByteDance API Key",
},
Endpoint: {
Title: "Adresa rozhraní",
SubTitle: "Příklad:",
},
},
Alibaba: {
ApiKey: {
Title: "Rozhraní klíč",
SubTitle: "Použijte vlastní Alibaba Cloud API Key",
Placeholder: "Alibaba Cloud API Key",
},
Endpoint: {
Title: "Adresa rozhraní",
SubTitle: "Příklad:",
},
},
CustomModel: {
Title: "Vlastní názvy modelů",
SubTitle: "Přidejte možnosti vlastních modelů, oddělené čárkami",
},
},
Model: "Model (model)",
CompressModel: {
Title: "Kompresní model",
SubTitle: "Model používaný pro kompresi historie",
},
Temperature: {
Title: "Teplota",
SubTitle: "Větší hodnota činí výstup náhodnějším",
Title: "Náhodnost (temperature)",
SubTitle: "Čím vyšší hodnota, tím náhodnější odpovědi",
},
TopP: {
Title: "Jádrové vzorkování (top_p)",
SubTitle: "Podobné náhodnosti, ale neměňte spolu s náhodností",
},
MaxTokens: {
Title: "Max. počet tokenů",
SubTitle: "Maximální délka vstupního tokenu a generovaných tokenů",
Title: "Omezení odpovědi (max_tokens)",
SubTitle: "Maximální počet Tokenů použitých v jednom interakci",
},
PresencePenalty: {
Title: "Přítomnostní korekce",
SubTitle: "Větší hodnota zvyšuje pravděpodobnost nových témat.",
Title: "Čerstvost témat (presence_penalty)",
SubTitle:
"Čím vyšší hodnota, tím větší pravděpodobnost rozšíření na nová témata",
},
FrequencyPenalty: {
Title: "Frekvenční penalizace",
Title: "Penalizace frekvence (frequency_penalty)",
SubTitle:
"Větší hodnota snižující pravděpodobnost opakování stejného řádku",
"Čím vyšší hodnota, tím větší pravděpodobnost snížení opakování slov",
},
},
Store: {
DefaultTopic: "Nová konverzace",
BotHello: "Ahoj! Jak mohu dnes pomoci?",
Error: "Něco se pokazilo, zkuste to prosím později.",
DefaultTopic: "Nový chat",
BotHello: "Jak vám mohu pomoci?",
Error: "Došlo k chybě, zkuste to prosím znovu později.",
Prompt: {
History: (content: string) =>
"Toto je shrnutí historie chatu mezi umělou inteligencí a uživatelem v podobě rekapitulace: " +
content,
"Toto je shrnutí historie chatu jako kontext: " + content,
Topic:
"Vytvořte prosím název o čtyřech až pěti slovech vystihující průběh našeho rozhovoru bez jakýchkoli úvodních slov, interpunkčních znamének, uvozovek, teček, symbolů nebo dalšího textu. Odstraňte uvozovky.",
"Použijte čtyři až pět slov pro stručné téma této věty, bez vysvětlení, interpunkce, citoslovcí, nadbytečného textu, bez tučného písma. Pokud téma neexistuje, vraťte pouze 'neformální chat'.",
Summarize:
"Krátce shrň naši diskusi v rozsahu do 200 slov a použij ji jako podnět pro budoucí kontext.",
"Stručně shrňte obsah konverzace jako kontextový prompt pro budoucí použití, do 200 slov",
},
},
Copy: {
Success: "Zkopírováno do schránky",
Failed: "Kopírování selhalo, prosím, povolte přístup ke schránce",
},
Download: {
Success: "Obsah byl stažen do vašeho adresáře.",
Failed: "Stahování selhalo.",
},
Context: {
Toast: (x: any) => `Použití ${x} kontextových pokynů`,
Edit: "Kontextové a paměťové pokyny",
Add: "Přidat pokyn",
Toast: (x: any) => `Obsahuje ${x} přednastavených promptů`,
Edit: "Nastavení aktuální konverzace",
Add: "Přidat novou konverzaci",
Clear: "Kontext byl vymazán",
Revert: "Obnovit kontext",
},
Plugin: {
Name: "Plugin",
},
FineTuned: {
Sysmessage: "Jste asistent, který",
Sysmessage: "Jste asistent",
},
SearchChat: {
Name: "Hledat",
Page: {
Title: "Hledat v historii chatu",
Search: "Zadejte hledané klíčové slovo",
NoResult: "Nebyly nalezeny žádné výsledky",
NoData: "Žádná data",
Loading: "Načítání",
SubTitle: (count: number) => `Nalezeno ${count} výsledků`,
},
Item: {
View: "Zobrazit",
},
},
Mask: {
Name: "Maska",
Page: {
Title: "Šablona pokynu",
SubTitle: (count: number) => `${count} šablon pokynů`,
Search: "Hledat v šablonách",
Create: "Vytvořit",
Title: "Přednastavené role masky",
SubTitle: (count: number) => `${count} definovaných rolí`,
Search: "Hledat role masky",
Create: "Nový",
},
Item: {
Info: (count: number) => `${count} pokynů`,
Info: (count: number) => `Obsahuje ${count} přednastavených konverzací`,
Chat: "Chat",
View: "Zobrazit",
Edit: "Upravit",
Delete: "Smazat",
DeleteConfirm: "Potvrdit smazání?",
DeleteConfirm: "Opravdu chcete smazat?",
},
EditModal: {
Title: (readonly: boolean) =>
`Editovat šablonu pokynu ${readonly ? "(pouze ke čtení)" : ""}`,
Download: "Stáhnout",
Clone: "Duplikovat",
`Upravit přednastavenou masku ${readonly ? " (jen pro čtení)" : ""}`,
Download: "Stáhnout přednastavení",
Clone: "Klonovat přednastavení",
},
Config: {
Avatar: "Avatar Bota",
Name: "Jméno Bota",
Avatar: "Profilový obrázek",
Name: "Název role",
Sync: {
Title: "Použít globální nastavení",
SubTitle: "Použít globální modelová nastavení pro aktuální konverzaci",
Confirm:
"Vaše vlastní nastavení konverzace bude automaticky přepsáno, opravdu chcete použít globální nastavení?",
},
HideContext: {
Title: "Skrýt přednastavené konverzace",
SubTitle:
"Po skrytí se přednastavené konverzace nebudou zobrazovat v chatovém rozhraní",
},
Share: {
Title: "Sdílet tuto masku",
SubTitle: "Generovat přímý odkaz na tuto masku",
Action: "Kopírovat odkaz",
},
},
},
NewChat: {
Return: "Zpět",
Skip: "Přeskočit",
Title: "Vyberte Masku",
SubTitle: "Chatovat s duší za Maskou",
More: "Najít více",
NotShow: "Nezobrazovat znovu",
ConfirmNoShow: "Potvrdit zakázáníMůžete jej povolit později v nastavení.",
Skip: "Začít hned",
NotShow: "Zobrazit už nikdy",
ConfirmNoShow:
"Opravdu chcete zakázat? Zakázání můžete kdykoli znovu povolit v nastavení.",
Title: "Vyberte masku",
SubTitle: "Začněte nyní a konfrontujte se s myslí za maskou",
More: "Zobrazit vše",
},
URLCommand: {
Code: "Byl detekován přístupový kód v odkazu, chcete jej automaticky vyplnit?",
Settings:
"Byla detekována přednastavená nastavení v odkazu, chcete je automaticky vyplnit?",
},
UI: {
Confirm: "Potvrdit",
Cancel: "Zrušit",
Close: "Zavřít",
Create: "Vytvořit",
Create: "Nový",
Edit: "Upravit",
Export: "Exportovat",
Import: "Importovat",
Sync: "Synchronizovat",
Config: "Konfigurovat",
},
Exporter: {
Description: {
Title: "Pouze zprávy po vymazání kontextu budou zobrazeny",
},
Model: "Model",
Messages: "Zprávy",
Topic: "Téma",

View File

@@ -1,75 +1,172 @@
import { SubmitKey } from "../store/config";
import type { PartialLocaleType } from "./index";
import { getClientConfig } from "../config/client";
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
const isApp = !!getClientConfig()?.isApp;
const de: PartialLocaleType = {
WIP: "In Bearbeitung...",
Error: {
Unauthorized:
"Unbefugter Zugriff, bitte geben Sie den Zugangscode auf der [Einstellungsseite](/#/auth) ein.",
Unauthorized: isApp
? `😆 Das Gespräch hatte einige Probleme, keine Sorge:
\\ 1⃣ Wenn du ohne Konfiguration sofort starten möchtest, [klicke hier, um sofort zu chatten 🚀](${SAAS_CHAT_UTM_URL})
\\ 2⃣ Wenn du deine eigenen OpenAI-Ressourcen verwenden möchtest, klicke [hier](/#/settings), um die Einstellungen zu ändern ⚙️`
: `😆 Das Gespräch hatte einige Probleme, keine Sorge:
\ 1⃣ Wenn du ohne Konfiguration sofort starten möchtest, [klicke hier, um sofort zu chatten 🚀](${SAAS_CHAT_UTM_URL})
\ 2⃣ Wenn du eine private Bereitstellung verwendest, klicke [hier](/#/auth), um den Zugriffsschlüssel einzugeben 🔑
\ 3⃣ Wenn du deine eigenen OpenAI-Ressourcen verwenden möchtest, klicke [hier](/#/settings), um die Einstellungen zu ändern ⚙️
`,
},
Auth: {
Title: "Passwort erforderlich",
Tips: "Der Administrator hat die Passwortüberprüfung aktiviert. Bitte geben Sie den Zugangscode unten ein.",
SubTips: "Oder geben Sie Ihren OpenAI oder Google API-Schlüssel ein.",
Input: "Geben Sie hier den Zugangscode ein",
Confirm: "Bestätigen",
Later: "Später",
Return: "Zurück",
SaasTips:
"Die Konfiguration ist zu kompliziert, ich möchte es sofort nutzen",
TopTips:
"🥳 NextChat AI Einführungsangebot, schalte jetzt OpenAI o1, GPT-4o, Claude-3.5 und die neuesten großen Modelle frei",
},
ChatItem: {
ChatItemCount: (count: number) => `${count} Nachrichten`,
ChatItemCount: (count: number) => `${count} Gespräche`,
},
Chat: {
SubTitle: (count: number) => `${count} Nachrichten mit ChatGPT`,
Actions: {
ChatList: "Zur Chat-Liste gehen",
CompressedHistory: "Komprimierter Gedächtnis-Prompt",
Export: "Alle Nachrichten als Markdown exportieren",
Copy: "Kopieren",
Stop: "Stop",
Retry: "Wiederholen",
Delete: "Delete",
SubTitle: (count: number) => `Insgesamt ${count} Gespräche`,
EditMessage: {
Title: "Nachricht bearbeiten",
Topic: {
Title: "Chat-Thema",
SubTitle: "Ändern Sie das aktuelle Chat-Thema",
},
},
Rename: "Chat umbenennen",
Typing: "Tippen...",
Actions: {
ChatList: "Nachrichtliste anzeigen",
CompressedHistory: "Komprimierte Historie anzeigen",
Export: "Chatverlauf exportieren",
Copy: "Kopieren",
Stop: "Stoppen",
Retry: "Erneut versuchen",
Pin: "Anheften",
PinToastContent: "1 Gespräch an den voreingestellten Prompt angeheftet",
PinToastAction: "Ansehen",
Delete: "Löschen",
Edit: "Bearbeiten",
RefreshTitle: "Titel aktualisieren",
RefreshToast: "Anfrage zur Titelaktualisierung gesendet",
},
Commands: {
new: "Neues Gespräch",
newm: "Neues Gespräch aus Maske erstellen",
next: "Nächstes Gespräch",
prev: "Vorheriges Gespräch",
clear: "Kontext löschen",
del: "Gespräch löschen",
},
InputActions: {
Stop: "Antwort stoppen",
ToBottom: "Zum neuesten Beitrag",
Theme: {
auto: "Automatisches Thema",
light: "Helles Thema",
dark: "Dunkles Thema",
},
Prompt: "Schnellbefehle",
Masks: "Alle Masken",
Clear: "Chat löschen",
Settings: "Gesprächseinstellungen",
UploadImage: "Bild hochladen",
},
Rename: "Gespräch umbenennen",
Typing: "Tippt…",
Input: (submitKey: string) => {
var inputHints = `${submitKey} um zu Senden`;
var inputHints = `${submitKey} senden`;
if (submitKey === String(SubmitKey.Enter)) {
inputHints += ", Umschalt + Eingabe für Zeilenumbruch";
inputHints += "Shift + Enter für Zeilenumbruch";
}
return inputHints + ", / zum Durchsuchen von Prompts";
return inputHints + "/ für Autovervollständigung, : für Befehle";
},
Send: "Senden",
Config: {
Reset: "Reset to Default",
SaveAs: "Save as Mask",
Reset: "Erinnerung löschen",
SaveAs: "Als Maske speichern",
},
IsContext: "Voreingestellter Prompt",
},
Export: {
Title: "Alle Nachrichten",
Title: "Chatverlauf teilen",
Copy: "Alles kopieren",
Download: "Herunterladen",
MessageFromYou: "Deine Nachricht",
MessageFromChatGPT: "Nachricht von ChatGPT",
Download: "Datei herunterladen",
Share: "Auf ShareGPT teilen",
MessageFromYou: "Benutzer",
MessageFromChatGPT: "ChatGPT",
Format: {
Title: "Exportformat",
SubTitle: "Kann als Markdown-Text oder PNG-Bild exportiert werden",
},
IncludeContext: {
Title: "Maske Kontext einbeziehen",
SubTitle: "Soll der Maskenkontext in den Nachrichten angezeigt werden?",
},
Steps: {
Select: "Auswählen",
Preview: "Vorschau",
},
Image: {
Toast: "Screenshot wird erstellt",
Modal: "Lang drücken oder Rechtsklick, um Bild zu speichern",
},
},
Select: {
Search: "Nachrichten suchen",
All: "Alles auswählen",
Latest: "Neueste",
Clear: "Auswahl aufheben",
},
Memory: {
Title: "Gedächtnis-Prompt",
EmptyContent: "Noch nichts.",
Send: "Gedächtnis senden",
Copy: "Gedächtnis kopieren",
Reset: "Sitzung zurücksetzen",
ResetConfirm:
"Das Zurücksetzen löscht den aktuellen Gesprächsverlauf und das Langzeit-Gedächtnis. Möchten Sie wirklich zurücksetzen?",
Title: "Historische Zusammenfassung",
EmptyContent:
"Gesprächsinhalte sind zu kurz, keine Zusammenfassung erforderlich",
Send: "Chatverlauf automatisch komprimieren und als Kontext senden",
Copy: "Zusammenfassung kopieren",
Reset: "[nicht verwendet]",
ResetConfirm: "Zusammenfassung löschen bestätigen?",
},
Home: {
NewChat: "Neuer Chat",
DeleteChat: "Bestätigen Sie, um das ausgewählte Gespräch zu löschen?",
DeleteToast: "Chat gelöscht",
Revert: "Zurücksetzen",
NewChat: "Neues Gespräch",
DeleteChat: "Bestätigen Sie das Löschen des ausgewählten Gesprächs?",
DeleteToast: "Gespräch gelöscht",
Revert: "Rückgängig machen",
},
Settings: {
Title: "Einstellungen",
SubTitle: "Alle Einstellungen",
SubTitle: "Alle Einstellungsmöglichkeiten",
Danger: {
Reset: {
Title: "Alle Einstellungen zurücksetzen",
SubTitle: "Setzt alle Einstellungen auf die Standardwerte zurück",
Action: "Jetzt zurücksetzen",
Confirm: "Bestätigen Sie das Zurücksetzen aller Einstellungen?",
},
Clear: {
Title: "Alle Daten löschen",
SubTitle: "Löscht alle Chats und Einstellungsdaten",
Action: "Jetzt löschen",
Confirm:
"Bestätigen Sie das Löschen aller Chats und Einstellungsdaten?",
},
},
Lang: {
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
Name: "Sprache", // ACHTUNG: Wenn Sie eine neue Übersetzung hinzufügen möchten, übersetzen Sie diesen Wert bitte nicht, lassen Sie ihn als `Sprache`
All: "Alle Sprachen",
},
Avatar: "Avatar",
FontSize: {
Title: "Schriftgröße",
SubTitle: "Schriftgröße des Chat-Inhalts anpassen",
SubTitle: "Schriftgröße des Chat-Inhalts",
},
FontFamily: {
Title: "Chat-Schriftart",
@@ -78,164 +175,431 @@ const de: PartialLocaleType = {
Placeholder: "Schriftartname",
},
InjectSystemPrompts: {
Title: "System-Prompts einfügen",
Title: "Systemweite Eingabeaufforderungen einfügen",
SubTitle:
"Erzwingt das Hinzufügen eines simulierten systemweiten Prompts von ChatGPT am Anfang der Nachrichtenliste bei jeder Anfrage",
"Fügt jeder Nachricht am Anfang der Nachrichtenliste eine simulierte ChatGPT-Systemaufforderung hinzu",
},
InputTemplate: {
Title: "Benutzer-Eingabeverarbeitung",
SubTitle:
"Die neueste Nachricht des Benutzers wird in diese Vorlage eingefügt",
},
Update: {
Version: (x: string) => `Version: ${x}`,
IsLatest: "Neueste Version",
CheckUpdate: "Update prüfen",
IsChecking: "Update wird geprüft...",
Version: (x: string) => `Aktuelle Version: ${x}`,
IsLatest: "Bereits die neueste Version",
CheckUpdate: "Auf Updates überprüfen",
IsChecking: "Überprüfe auf Updates...",
FoundUpdate: (x: string) => `Neue Version gefunden: ${x}`,
GoToUpdate: "Aktualisieren",
GoToUpdate: "Zum Update gehen",
},
SendKey: "Senden-Taste",
Theme: "Erscheinungsbild",
TightBorder: "Enger Rahmen",
SendKey: "Sende-Taste",
Theme: "Thema",
TightBorder: "Randloser Modus",
SendPreviewBubble: {
Title: "Vorschau-Bubble senden",
SubTitle: "Preview markdown in bubble",
Title: "Vorschau-Bubble",
SubTitle: "Markdown-Inhalt in der Vorschau-Bubble anzeigen",
},
AutoGenerateTitle: {
Title: "Titel automatisch generieren",
SubTitle:
"Basierend auf dem Chat-Inhalt einen passenden Titel generieren",
},
Sync: {
CloudState: "Cloud-Daten",
NotSyncYet: "Noch nicht synchronisiert",
Success: "Synchronisation erfolgreich",
Fail: "Synchronisation fehlgeschlagen",
Config: {
Modal: {
Title: "Cloud-Synchronisation konfigurieren",
Check: "Verfügbarkeit überprüfen",
},
SyncType: {
Title: "Synchronisationstyp",
SubTitle: "Wählen Sie den bevorzugten Synchronisationsserver aus",
},
Proxy: {
Title: "Proxy aktivieren",
SubTitle:
"Beim Synchronisieren im Browser muss ein Proxy aktiviert werden, um Cross-Origin-Beschränkungen zu vermeiden",
},
ProxyUrl: {
Title: "Proxy-Adresse",
SubTitle: "Nur für projektinterne Cross-Origin-Proxy",
},
WebDav: {
Endpoint: "WebDAV-Adresse",
UserName: "Benutzername",
Password: "Passwort",
},
UpStash: {
Endpoint: "UpStash Redis REST-Url",
UserName: "Sicherungsname",
Password: "UpStash Redis REST-Token",
},
},
LocalState: "Lokale Daten",
Overview: (overview: any) => {
return `${overview.chat} Chats, ${overview.message} Nachrichten, ${overview.prompt} Eingabeaufforderungen, ${overview.mask} Masken`;
},
ImportFailed: "Import fehlgeschlagen",
},
Mask: {
Splash: {
Title: "Mask Splash Screen",
SubTitle: "Show a mask splash screen before starting new chat",
Title: "Masken-Startseite",
SubTitle:
"Zeige die Masken-Startseite beim Erstellen eines neuen Chats",
},
Builtin: {
Title: "Eingebaute Masken ausblenden",
SubTitle: "Blendet eingebaute Masken in allen Maskenlisten aus",
},
},
Prompt: {
Disable: {
Title: "Autovervollständigung deaktivieren",
SubTitle: "Autovervollständigung mit / starten",
Title: "Automatische Eingabeaufforderung deaktivieren",
SubTitle:
"Geben Sie am Anfang des Eingabefelds / ein, um die automatische Vervollständigung auszulösen",
},
List: "Prompt-Liste",
List: "Benutzerdefinierte Eingabeaufforderungsliste",
ListCount: (builtin: number, custom: number) =>
`${builtin} integriert, ${custom} benutzerdefiniert`,
`Eingebaut ${builtin} Stück, Benutzerdefiniert ${custom} Stück`,
Edit: "Bearbeiten",
Modal: {
Title: "Prompt List",
Add: "Add One",
Search: "Search Prompts",
Title: "Eingabeaufforderungsliste",
Add: "Neu erstellen",
Search: "Eingabeaufforderungen suchen",
},
EditModal: {
Title: "Edit Prompt",
Title: "Eingabeaufforderung bearbeiten",
},
},
HistoryCount: {
Title: "Anzahl der angehängten Nachrichten",
SubTitle: "Anzahl der pro Anfrage angehängten gesendeten Nachrichten",
Title: "Anzahl der historischen Nachrichten",
SubTitle:
"Anzahl der historischen Nachrichten, die bei jeder Anfrage mitgesendet werden",
},
CompressThreshold: {
Title: "Schwellenwert für Verlaufskomprimierung",
Title: "Komprimierungsschwelle für historische Nachrichtenlänge",
SubTitle:
"Komprimierung, wenn die Länge der unkomprimierten Nachrichten den Wert überschreitet",
"Wenn die unkomprimierten historischen Nachrichten diesen Wert überschreiten, wird komprimiert",
},
Usage: {
Title: "Kontostand",
Title: "Guthabenabfrage",
SubTitle(used: any, total: any) {
return `Diesen Monat ausgegeben $${used}, Abonnement $${total}`;
return `In diesem Monat verwendet $${used}, Abonnement insgesamt $${total}`;
},
IsChecking: "Wird überprüft...",
Check: "Erneut prüfen",
NoAccess: "API-Schlüssel eingeben, um den Kontostand zu überprüfen",
IsChecking: "Wird überprüft",
Check: "Erneut überprüfen",
NoAccess:
"Geben Sie API-Schlüssel oder Zugangspasswort ein, um das Guthaben einzusehen",
},
Access: {
SaasStart: {
Title: "NextChat AI verwenden",
Label: "(Die kosteneffektivste Lösung)",
SubTitle:
"Offiziell von NextChat verwaltet, sofort einsatzbereit ohne Konfiguration, unterstützt die neuesten großen Modelle wie OpenAI o1, GPT-4o und Claude-3.5",
ChatNow: "Jetzt chatten",
},
AccessCode: {
Title: "Zugangscode",
SubTitle:
"Der Administrator hat die verschlüsselte Zugriffskontrolle aktiviert",
Placeholder: "Geben Sie den Zugangscode ein",
},
CustomEndpoint: {
Title: "Benutzerdefinierte Schnittstelle",
SubTitle: "Benutzerdefinierte Azure- oder OpenAI-Dienste verwenden",
},
Provider: {
Title: "Modellanbieter",
SubTitle: "Wechseln Sie zu verschiedenen Anbietern",
},
OpenAI: {
ApiKey: {
Title: "API-Schlüssel",
SubTitle:
"Verwenden Sie benutzerdefinierten OpenAI-Schlüssel, um Passwortzugangsbeschränkungen zu umgehen",
Placeholder: "OpenAI API-Schlüssel",
},
Endpoint: {
Title: "Schnittstellenadresse",
SubTitle: "Neben der Standardadresse muss http(s):// enthalten sein",
},
},
Azure: {
ApiKey: {
Title: "Schnittstellenschlüssel",
SubTitle:
"Verwenden Sie benutzerdefinierten Azure-Schlüssel, um Passwortzugangsbeschränkungen zu umgehen",
Placeholder: "Azure API-Schlüssel",
},
Endpoint: {
Title: "Schnittstellenadresse",
SubTitle: "Beispiel:",
},
ApiVerion: {
Title: "Schnittstellenversion (azure api version)",
SubTitle: "Wählen Sie eine spezifische Teilversion aus",
},
},
Anthropic: {
ApiKey: {
Title: "Schnittstellenschlüssel",
SubTitle:
"Verwenden Sie benutzerdefinierten Anthropic-Schlüssel, um Passwortzugangsbeschränkungen zu umgehen",
Placeholder: "Anthropic API-Schlüssel",
},
Endpoint: {
Title: "Schnittstellenadresse",
SubTitle: "Beispiel:",
},
ApiVerion: {
Title: "Schnittstellenversion (claude api version)",
SubTitle: "Wählen Sie eine spezifische API-Version aus",
},
},
Google: {
ApiKey: {
Title: "API-Schlüssel",
SubTitle: "Holen Sie sich Ihren API-Schlüssel von Google AI",
Placeholder: "Geben Sie Ihren Google AI Studio API-Schlüssel ein",
},
Endpoint: {
Title: "Endpunktadresse",
SubTitle: "Beispiel:",
},
ApiVersion: {
Title: "API-Version (nur für gemini-pro)",
SubTitle: "Wählen Sie eine spezifische API-Version aus",
},
GoogleSafetySettings: {
Title: "Google Sicherheitsfilterstufe",
SubTitle: "Inhaltfilterstufe einstellen",
},
},
Baidu: {
ApiKey: {
Title: "API-Schlüssel",
SubTitle: "Verwenden Sie benutzerdefinierten Baidu API-Schlüssel",
Placeholder: "Baidu API-Schlüssel",
},
SecretKey: {
Title: "Geheimschlüssel",
SubTitle: "Verwenden Sie benutzerdefinierten Baidu Geheimschlüssel",
Placeholder: "Baidu Geheimschlüssel",
},
Endpoint: {
Title: "Schnittstellenadresse",
SubTitle:
"Keine benutzerdefinierten Adressen unterstützen, konfigurieren Sie in .env",
},
},
ByteDance: {
ApiKey: {
Title: "Schnittstellenschlüssel",
SubTitle: "Verwenden Sie benutzerdefinierten ByteDance API-Schlüssel",
Placeholder: "ByteDance API-Schlüssel",
},
Endpoint: {
Title: "Schnittstellenadresse",
SubTitle: "Beispiel:",
},
},
Alibaba: {
ApiKey: {
Title: "Schnittstellenschlüssel",
SubTitle:
"Verwenden Sie benutzerdefinierten Alibaba Cloud API-Schlüssel",
Placeholder: "Alibaba Cloud API-Schlüssel",
},
Endpoint: {
Title: "Schnittstellenadresse",
SubTitle: "Beispiel:",
},
},
CustomModel: {
Title: "Benutzerdefinierter Modellname",
SubTitle:
"Fügen Sie benutzerdefinierte Modelloptionen hinzu, getrennt durch Kommas",
},
},
Model: "Modell",
CompressModel: {
Title: "Kompressionsmodell",
SubTitle: "Modell zur Komprimierung des Verlaufs",
},
Temperature: {
Title: "Temperature", //Temperatur
SubTitle: "Ein größerer Wert führt zu zufälligeren Antworten",
Title: "Zufälligkeit (temperature)",
SubTitle: "Je höher der Wert, desto zufälliger die Antwort",
},
TopP: {
Title: "Kern-Sampling (top_p)",
SubTitle:
"Ähnlich der Zufälligkeit, aber nicht zusammen mit Zufälligkeit ändern",
},
MaxTokens: {
Title: "Max Tokens", //Maximale Token
SubTitle: "Maximale Anzahl der Anfrage- plus Antwort-Token",
Title: "Maximale Token-Anzahl pro Antwort",
SubTitle: "Maximale Anzahl der Tokens pro Interaktion",
},
PresencePenalty: {
Title: "Presence Penalty", //Anwesenheitsstrafe
Title: "Themenfrische (presence_penalty)",
SubTitle:
"Ein größerer Wert erhöht die Wahrscheinlichkeit, dass über neue Themen gesprochen wird",
"Je höher der Wert, desto wahrscheinlicher wird auf neue Themen eingegangen",
},
FrequencyPenalty: {
Title: "Frequency Penalty", // HäufigkeitStrafe
Title: "Häufigkeitsstrafe (frequency_penalty)",
SubTitle:
"Ein größerer Wert, der die Wahrscheinlichkeit verringert, dass dieselbe Zeile wiederholt wird",
"Je höher der Wert, desto wahrscheinlicher werden wiederholte Wörter reduziert",
},
},
Store: {
DefaultTopic: "Neues Gespräch",
BotHello: "Hallo! Wie kann ich Ihnen heute helfen?",
DefaultTopic: "Neuer Chat",
BotHello: "Wie kann ich Ihnen helfen?",
Error:
"Etwas ist schief gelaufen, bitte versuchen Sie es später noch einmal.",
"Ein Fehler ist aufgetreten, bitte versuchen Sie es später noch einmal",
Prompt: {
History: (content: string) =>
"Dies ist eine Zusammenfassung des Chatverlaufs zwischen dem KI und dem Benutzer als Rückblick: " +
"Dies ist eine Zusammenfassung des bisherigen Chats als Hintergrundinformation: " +
content,
Topic:
"Bitte erstellen Sie einen vier- bis fünfwörtigen Titel, der unser Gespräch zusammenfasst, ohne Einleitung, Zeichensetzung, Anführungszeichen, Punkte, Symbole oder zusätzlichen Text. Entfernen Sie Anführungszeichen.",
"Geben Sie ein kurzes Thema in vier bis fünf Wörtern zurück, ohne Erklärungen, ohne Satzzeichen, ohne Füllwörter, ohne zusätzliche Texte und ohne Fettdruck. Wenn kein Thema vorhanden ist, geben Sie bitte „Allgemeines Gespräch“ zurück.",
Summarize:
"Fassen Sie unsere Diskussion kurz in 200 Wörtern oder weniger zusammen, um sie als Pronpt für zukünftige Gespräche zu verwenden.",
"Fassen Sie den Gesprächsinhalt zusammen, um als Kontextaufforderung für den nächsten Schritt zu dienen, halten Sie es unter 200 Zeichen",
},
},
Copy: {
Success: "In die Zwischenablage kopiert",
Success: "In die Zwischenablage geschrieben",
Failed:
"Kopieren fehlgeschlagen, bitte geben Sie die Berechtigung zum Zugriff auf die Zwischenablage frei",
"Kopieren fehlgeschlagen, bitte erlauben Sie Zugriff auf die Zwischenablage",
},
Download: {
Success: "Inhalt wurde in Ihrem Verzeichnis heruntergeladen.",
Failed: "Download fehlgeschlagen.",
},
Context: {
Toast: (x: any) => `Mit ${x} Kontext-Prompts`,
Edit: "Kontext- und Gedächtnis-Prompts",
Add: "Hinzufügen",
Toast: (x: any) => `Beinhaltet ${x} vordefinierte Eingabeaufforderungen`,
Edit: "Aktuelle Gesprächseinstellungen",
Add: "Neues Gespräch hinzufügen",
Clear: "Kontext gelöscht",
Revert: "Kontext wiederherstellen",
},
Plugin: {
Name: "Plugin",
Name: "Plugins",
},
FineTuned: {
Sysmessage: "Du bist ein Assistent, der",
Sysmessage: "Du bist ein Assistent",
},
Mask: {
Name: "Mask",
SearchChat: {
Name: "Suche",
Page: {
Title: "Prompt Template",
SubTitle: (count: number) => `${count} prompt templates`,
Search: "Search Templates",
Create: "Create",
Title: "Chatverlauf durchsuchen",
Search: "Suchbegriff eingeben",
NoResult: "Keine Ergebnisse gefunden",
NoData: "Keine Daten",
Loading: "Laden",
SubTitle: (count: number) => `${count} Ergebnisse gefunden`,
},
Item: {
Info: (count: number) => `${count} prompts`,
Chat: "Chat",
View: "View",
Edit: "Edit",
Delete: "Delete",
DeleteConfirm: "Confirm to delete?",
View: "Ansehen",
},
},
Mask: {
Name: "Masken",
Page: {
Title: "Vordefinierte Rollenmasken",
SubTitle: (count: number) =>
`${count} vordefinierte Rollenbeschreibungen`,
Search: "Rollenmasken suchen",
Create: "Neu erstellen",
},
Item: {
Info: (count: number) => `Beinhaltet ${count} vordefinierte Gespräche`,
Chat: "Gespräch",
View: "Anzeigen",
Edit: "Bearbeiten",
Delete: "Löschen",
DeleteConfirm: "Bestätigen Sie das Löschen?",
},
EditModal: {
Title: (readonly: boolean) =>
`Edit Prompt Template ${readonly ? "(readonly)" : ""}`,
Download: "Download",
Clone: "Clone",
`Vordefinierte Maske bearbeiten ${readonly ? "Nur lesen" : ""}`,
Download: "Vorgabe herunterladen",
Clone: "Vorgabe klonen",
},
Config: {
Avatar: "Bot Avatar",
Name: "Bot Name",
Avatar: "Rollen-Avatar",
Name: "Rollenname",
Sync: {
Title: "Globale Einstellungen verwenden",
SubTitle:
"Soll das aktuelle Gespräch die globalen Modelleinstellungen verwenden?",
Confirm:
"Die benutzerdefinierten Einstellungen des aktuellen Gesprächs werden automatisch überschrieben. Bestätigen Sie, dass Sie die globalen Einstellungen aktivieren möchten?",
},
HideContext: {
Title: "Vordefinierte Gespräche ausblenden",
SubTitle:
"Nach dem Ausblenden werden vordefinierte Gespräche nicht mehr im Chat angezeigt",
},
Share: {
Title: "Diese Maske teilen",
SubTitle: "Generieren Sie einen Direktlink zu dieser Maske",
Action: "Link kopieren",
},
},
},
NewChat: {
Return: "Return",
Skip: "Skip",
Title: "Pick a Mask",
SubTitle: "Chat with the Soul behind the Mask",
More: "Find More",
NotShow: "Not Show Again",
ConfirmNoShow: "Confirm to disableYou can enable it in settings later.",
Return: "Zurück",
Skip: "Direkt beginnen",
NotShow: "Nicht mehr anzeigen",
ConfirmNoShow:
"Bestätigen Sie die Deaktivierung? Nach der Deaktivierung können Sie jederzeit in den Einstellungen wieder aktivieren.",
Title: "Wählen Sie eine Maske aus",
SubTitle:
"Starten Sie jetzt und lassen Sie sich von den Gedanken hinter der Maske inspirieren",
More: "Alle anzeigen",
},
URLCommand: {
Code: "Ein Zugangscode wurde im Link gefunden. Möchten Sie diesen automatisch einfügen?",
Settings:
"Vordefinierte Einstellungen wurden im Link gefunden. Möchten Sie diese automatisch einfügen?",
},
UI: {
Confirm: "Confirm",
Cancel: "Cancel",
Close: "Close",
Create: "Create",
Edit: "Edit",
Confirm: "Bestätigen",
Cancel: "Abbrechen",
Close: "Schließen",
Create: "Neu erstellen",
Edit: "Bearbeiten",
Export: "Exportieren",
Import: "Importieren",
Sync: "Synchronisieren",
Config: "Konfigurieren",
},
Exporter: {
Description: {
Title: "Nur Nachrichten nach dem Löschen des Kontexts werden angezeigt",
},
Model: "Modell",
Messages: "Nachrichten",
Topic: "Thema",

Some files were not shown because too many files have changed in this diff Show More