Compare commits

...

421 Commits

Author SHA1 Message Date
Dogtiti
8a22c9d6db feature: support glm-4v 2024-12-28 23:33:06 +08:00
RiverRay
5f96804f3b Merge pull request #5920 from fishshi/i18n
Use i18n for DISCOVERY
2024-12-28 22:05:37 +08:00
RiverRay
13430ea3e2 Merge pull request #5965 from zmhuanf/temp
Fix issue #5964: Prevents character loss in gemini-2.0-flash-thinking-exp-1219 responses
2024-12-28 22:02:02 +08:00
Dogtiti
9df24e568b Merge pull request #5996 from ChatGPTNextWeb/feature/cogview
Feature/cogview
2024-12-28 20:25:25 +08:00
Dogtiti
bc322be448 fix: type error 2024-12-28 20:24:08 +08:00
Dogtiti
a867adaf04 fix: size 2024-12-28 20:23:51 +08:00
Dogtiti
0cb186846a feature: support glm Cogview 2024-12-28 20:23:44 +08:00
Dogtiti
e467ce028d Merge pull request #5994 from ConnectAI-E/fix/failed-test
fix: failed unit test
2024-12-28 17:55:29 +08:00
Dogtiti
cdfe907fb5 fix: failed unit test 2024-12-28 17:54:21 +08:00
Dogtiti
d91af7f983 Merge pull request #5883 from code-october/fix/model-leak
fix model leak issue
2024-12-28 14:47:35 +08:00
RiverRay
0c3d4462ca Merge pull request #5976 from ChatGPTNextWeb/Leizhenpeng-patch-1
Update README.md
2024-12-23 22:47:59 +08:00
RiverRay
3c859fc29f Update README.md 2024-12-23 22:47:16 +08:00
zmhuanf
87b5e3bf62 修复bug; 2024-12-22 15:44:47 +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
fishshi
93c5320bf2 Use i18n for DISCOVERY 2024-12-10 15:56:04 +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
code-october
cc5e16b045 update unit test 2024-11-30 07:30:52 +00:00
code-october
54f6feb2d7 update unit test 2024-11-30 07:28:38 +00:00
code-october
e1ac0538b8 add unit test 2024-11-30 07:22:24 +00:00
code-october
1a678cb4d8 fix model leak issue 2024-11-29 15:47:28 +00: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
l.tingting
c1b74201e4 add chatgpt-4o-latest 2024-09-07 01:42:56 +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
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
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
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
butterfly
4d6b981a54 bugfix: Delete the escapeDollarNumber function, which causes errors in rendering a latex string 2024-03-26 11:43:55 +08:00
155 changed files with 9074 additions and 1532 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,7 @@ 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)
@@ -60,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
@@ -93,10 +97,12 @@ For enterprise inquiries, please contact: **business@nextchat.dev**
- [x] Artifacts: Easily preview, copy and share generated content/webpages through a separate window [#5092](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/pull/5092)
- [x] Plugins: support network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353)
- [x] network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353)
- [x] Supports Realtime Chat [#5672](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5672)
- [ ] local knowledge base
## What's New
- 🚀 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.
@@ -130,10 +136,12 @@ For enterprise inquiries, please contact: **business@nextchat.dev**
- [x] Artifacts: 通过独立窗口,轻松预览、复制和分享生成的内容/可交互网页 [#5092](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/pull/5092)
- [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 模型。
@@ -172,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:
@@ -296,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
@@ -329,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.
@@ -340,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
@@ -392,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,8 @@ 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(
@@ -38,6 +40,10 @@ async function handle(
return stabilityHandler(req, { params });
case ApiPath.Iflytek:
return iflytekHandler(req, { params });
case ApiPath.XAI:
return xaiHandler(req, { params });
case ApiPath.ChatGLM:
return chatglmHandler(req, { params });
case ApiPath.OpenAI:
return openaiHandler(req, { params });
default:

View File

@@ -1,6 +1,5 @@
import { getServerSideConfig } from "@/app/config/server";
import {
Alibaba,
ALIBABA_BASE_URL,
ApiPath,
ModelProvider,
@@ -9,8 +8,7 @@ import {
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";
import { isModelNotavailableInServer } from "@/app/utils/model";
const serverConfig = getServerSideConfig();
@@ -91,7 +89,7 @@ async function request(req: NextRequest) {
// not undefined and is false
if (
isModelAvailableInServer(
isModelNotavailableInServer(
serverConfig.customModels,
jsonBody?.model as string,
ServiceProvider.Alibaba as string,

View File

@@ -3,14 +3,13 @@ import {
ANTHROPIC_BASE_URL,
Anthropic,
ApiPath,
DEFAULT_MODELS,
ServiceProvider,
ModelProvider,
} from "@/app/constant";
import { prettyObject } from "@/app/utils/format";
import { NextRequest, NextResponse } from "next/server";
import { auth } from "./auth";
import { isModelAvailableInServer } from "@/app/utils/model";
import { isModelNotavailableInServer } from "@/app/utils/model";
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
const ALLOWD_PATH = new Set([Anthropic.ChatPath, Anthropic.ChatPath1]);
@@ -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") ||
@@ -122,7 +122,7 @@ async function request(req: NextRequest) {
// not undefined and is false
if (
isModelAvailableInServer(
isModelNotavailableInServer(
serverConfig.customModels,
jsonBody?.model as string,
ServiceProvider.Anthropic as string,

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,13 +3,12 @@ import {
BAIDU_BASE_URL,
ApiPath,
ModelProvider,
BAIDU_OATUH_URL,
ServiceProvider,
} from "@/app/constant";
import { prettyObject } from "@/app/utils/format";
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/app/api/auth";
import { isModelAvailableInServer } from "@/app/utils/model";
import { isModelNotavailableInServer } from "@/app/utils/model";
import { getAccessToken } from "@/app/utils/baidu";
const serverConfig = getServerSideConfig();
@@ -105,7 +104,7 @@ async function request(req: NextRequest) {
// not undefined and is false
if (
isModelAvailableInServer(
isModelNotavailableInServer(
serverConfig.customModels,
jsonBody?.model as string,
ServiceProvider.Baidu as string,

View File

@@ -8,7 +8,7 @@ import {
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 { isModelNotavailableInServer } from "@/app/utils/model";
const serverConfig = getServerSideConfig();
@@ -88,7 +88,7 @@ async function request(req: NextRequest) {
// not undefined and is false
if (
isModelAvailableInServer(
isModelNotavailableInServer(
serverConfig.customModels,
jsonBody?.model as string,
ServiceProvider.ByteDance as string,

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, isModelNotavailableInServer } from "../utils/model";
const serverConfig = getServerSideConfig();
@@ -76,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/",
@@ -123,15 +118,14 @@ export async function requestOpenai(req: NextRequest) {
// not undefined and is false
if (
isModelAvailableInServer(
isModelNotavailableInServer(
serverConfig.customModels,
jsonBody?.model as string,
ServiceProvider.OpenAI as string,
) ||
isModelAvailableInServer(
serverConfig.customModels,
jsonBody?.model as string,
ServiceProvider.Azure as string,
[
ServiceProvider.OpenAI,
ServiceProvider.Azure,
jsonBody?.model as string, // support provider-unspecified model
],
)
) {
return NextResponse.json(

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

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,
@@ -9,8 +8,7 @@ import {
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";
import { isModelNotavailableInServer } from "@/app/utils/model";
// iflytek
const serverConfig = getServerSideConfig();
@@ -91,7 +89,7 @@ async function request(req: NextRequest) {
// not undefined and is false
if (
isModelAvailableInServer(
isModelNotavailableInServer(
serverConfig.customModels,
jsonBody?.model as string,
ServiceProvider.Iflytek as string,

View File

@@ -1,6 +1,5 @@
import { getServerSideConfig } from "@/app/config/server";
import {
Moonshot,
MOONSHOT_BASE_URL,
ApiPath,
ModelProvider,
@@ -9,8 +8,7 @@ import {
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";
import { isModelNotavailableInServer } from "@/app/utils/model";
const serverConfig = getServerSideConfig();
@@ -90,7 +88,7 @@ async function request(req: NextRequest) {
// not undefined and is false
if (
isModelAvailableInServer(
isModelNotavailableInServer(
serverConfig.customModels,
jsonBody?.model as string,
ServiceProvider.Moonshot as string,

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.id.startsWith("gpt-4o-mini"),
(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(
{

View File

@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
import { getServerSideConfig } from "@/app/config/server";
export async function handle(
req: NextRequest,
@@ -9,6 +10,7 @@ export async function handle(
if (req.method === "OPTIONS") {
return NextResponse.json({ body: "OK" }, { status: 200 });
}
const serverConfig = getServerSideConfig();
// remove path params from searchParams
req.nextUrl.searchParams.delete("path");
@@ -31,6 +33,18 @@ export async function handle(
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,

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

View File

@@ -1,7 +1,6 @@
import { getClientConfig } from "../config/client";
import {
ACCESS_CODE_PREFIX,
Azure,
ModelProvider,
ServiceProvider,
} from "../constant";
@@ -21,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 {
@@ -54,12 +56,21 @@ export interface LLMConfig {
style?: DalleRequestPayload["style"];
}
export interface SpeechOptions {
model: string;
input: string;
voice: string;
response_format?: string;
speed?: number;
onController?: (controller: AbortController) => void;
}
export interface ChatOptions {
messages: RequestMessage[];
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;
@@ -88,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[]>;
}
@@ -142,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();
}
@@ -206,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;
@@ -226,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
@@ -239,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
@@ -253,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 {
@@ -270,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;
@@ -308,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,5 +1,5 @@
import { ACCESS_CODE_PREFIX, Anthropic, ApiPath } from "@/app/constant";
import { ChatOptions, getHeaders, LLMApi, MultimodalContent } from "../api";
import { Anthropic, ApiPath } from "@/app/constant";
import { ChatOptions, getHeaders, LLMApi, SpeechOptions } from "../api";
import {
useAccessStore,
useAppConfig,
@@ -8,18 +8,12 @@ import {
ChatMessageTool,
} from "@/app/store";
import { getClientConfig } from "@/app/config/client";
import { DEFAULT_API_HOST } from "@/app/constant";
import {
EventStreamContentType,
fetchEventSource,
} from "@fortaine/fetch-event-source";
import Locale from "../../locales";
import { prettyObject } from "@/app/utils/format";
import { ANTHROPIC_BASE_URL } from "@/app/constant";
import { getMessageTextContent, isVisionModel } from "@/app/utils";
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";
@@ -80,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);
@@ -319,13 +317,14 @@ export class ClaudeApi implements LLMApi {
};
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);
@@ -391,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);

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

@@ -0,0 +1,293 @@
"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, isVisionModel } from "@/app/utils";
import { RequestPayload } from "./openai";
import { fetch } from "@/app/utils/stream";
import { preProcessImageContent } from "@/app/utils/chat";
interface BasePayload {
model: string;
}
interface ChatPayload extends BasePayload {
messages: ChatOptions["messages"];
stream?: boolean;
temperature?: number;
presence_penalty?: number;
frequency_penalty?: number;
top_p?: number;
}
interface ImageGenerationPayload extends BasePayload {
prompt: string;
size?: string;
user_id?: string;
}
interface VideoGenerationPayload extends BasePayload {
prompt: string;
duration?: number;
resolution?: string;
user_id?: string;
}
type ModelType = "chat" | "image" | "video";
export class ChatGLMApi implements LLMApi {
private disableListModels = true;
private getModelType(model: string): ModelType {
if (model.startsWith("cogview-")) return "image";
if (model.startsWith("cogvideo-")) return "video";
return "chat";
}
private getModelPath(type: ModelType): string {
switch (type) {
case "image":
return ChatGLM.ImagePath;
case "video":
return ChatGLM.VideoPath;
default:
return ChatGLM.ChatPath;
}
}
private createPayload(
messages: ChatOptions["messages"],
modelConfig: any,
options: ChatOptions,
): BasePayload {
const modelType = this.getModelType(modelConfig.model);
const lastMessage = messages[messages.length - 1];
const prompt =
typeof lastMessage.content === "string"
? lastMessage.content
: lastMessage.content.map((c) => c.text).join("\n");
switch (modelType) {
case "image":
return {
model: modelConfig.model,
prompt,
size: options.config.size,
} as ImageGenerationPayload;
default:
return {
messages,
stream: options.config.stream,
model: modelConfig.model,
temperature: modelConfig.temperature,
presence_penalty: modelConfig.presence_penalty,
frequency_penalty: modelConfig.frequency_penalty,
top_p: modelConfig.top_p,
} as ChatPayload;
}
}
private parseResponse(modelType: ModelType, json: any): string {
switch (modelType) {
case "image": {
const imageUrl = json.data?.[0]?.url;
return imageUrl ? `![Generated Image](${imageUrl})` : "";
}
case "video": {
const videoUrl = json.data?.[0]?.url;
return videoUrl ? `<video controls src="${videoUrl}"></video>` : "";
}
default:
return this.extractMessage(json);
}
}
path(path: string): string {
const accessStore = useAccessStore.getState();
let baseUrl = "";
if (accessStore.useCustomConfig) {
baseUrl = accessStore.chatglmUrl;
}
if (baseUrl.length === 0) {
const isApp = !!getClientConfig()?.isApp;
const apiPath = ApiPath.ChatGLM;
baseUrl = isApp ? CHATGLM_BASE_URL : apiPath;
}
if (baseUrl.endsWith("/")) {
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
}
if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.ChatGLM)) {
baseUrl = "https://" + baseUrl;
}
console.log("[Proxy Endpoint] ", baseUrl, path);
return [baseUrl, path].join("/");
}
extractMessage(res: any) {
return res.choices?.at(0)?.message?.content ?? "";
}
speech(options: SpeechOptions): Promise<ArrayBuffer> {
throw new Error("Method not implemented.");
}
async chat(options: ChatOptions) {
const visionModel = isVisionModel(options.config.model);
const messages: ChatOptions["messages"] = [];
for (const v of options.messages) {
const content = visionModel
? await preProcessImageContent(v.content)
: getMessageTextContent(v);
messages.push({ role: v.role, content });
}
const modelConfig = {
...useAppConfig.getState().modelConfig,
...useChatStore.getState().currentSession().mask.modelConfig,
...{
model: options.config.model,
providerName: options.config.providerName,
},
};
const modelType = this.getModelType(modelConfig.model);
const requestPayload = this.createPayload(messages, modelConfig, options);
const path = this.path(this.getModelPath(modelType));
console.log(`[Request] glm ${modelType} payload: `, requestPayload);
const controller = new AbortController();
options.onController?.(controller);
try {
const chatPayload = {
method: "POST",
body: JSON.stringify(requestPayload),
signal: controller.signal,
headers: getHeaders(),
};
const requestTimeoutId = setTimeout(
() => controller.abort(),
REQUEST_TIMEOUT_MS,
);
if (modelType === "image" || modelType === "video") {
const res = await fetch(path, chatPayload);
clearTimeout(requestTimeoutId);
const resJson = await res.json();
console.log(`[Response] glm ${modelType}:`, resJson);
const message = this.parseResponse(modelType, resJson);
options.onFinish(message, res);
return;
}
const shouldStream = !!options.config.stream;
if (shouldStream) {
const [tools, funcs] = usePluginStore
.getState()
.getAsTools(
useChatStore.getState().currentSession().mask?.plugin || [],
);
return stream(
path,
requestPayload,
getHeaders(),
tools as any,
funcs,
controller,
// parseSSE
(text: string, runTools: ChatMessageTool[]) => {
const json = JSON.parse(text);
const choices = json.choices as Array<{
delta: {
content: string;
tool_calls: ChatMessageTool[];
};
}>;
const tool_calls = choices[0]?.delta?.tool_calls;
if (tool_calls?.length > 0) {
const index = tool_calls[0]?.index;
const id = tool_calls[0]?.id;
const args = tool_calls[0]?.function?.arguments;
if (id) {
runTools.push({
id,
type: tool_calls[0]?.type,
function: {
name: tool_calls[0]?.function?.name as string,
arguments: args,
},
});
} else {
// @ts-ignore
runTools[index]["function"]["arguments"] += args;
}
}
return choices[0]?.delta?.content;
},
// processToolMessage
(
requestPayload: RequestPayload,
toolCallMessage: any,
toolCallResult: any[],
) => {
// @ts-ignore
requestPayload?.messages?.splice(
// @ts-ignore
requestPayload?.messages?.length,
0,
toolCallMessage,
...toolCallResult,
);
},
options,
);
} else {
const res = await fetch(path, chatPayload);
clearTimeout(requestTimeoutId);
const resJson = await res.json();
const message = this.extractMessage(resJson);
options.onFinish(message, res);
}
} catch (e) {
console.log("[Request] failed to make a chat request", e);
options.onError?.(e as Error);
}
}
async usage() {
return {
used: 0,
total: 0,
};
}
async models(): Promise<LLMModel[]> {
return [];
}
}

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,23 +51,35 @@ 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) {
console.log("[Response] gemini-pro response: ", res);
const getTextFromParts = (parts: any[]) => {
if (!Array.isArray(parts)) return "";
return parts
.map((part) => part?.text || "")
.filter((text) => text.trim() !== "")
.join("\n\n");
};
return (
res?.candidates?.at(0)?.content?.parts.at(0)?.text ||
getTextFromParts(res?.candidates?.at(0)?.content?.parts) ||
getTextFromParts(res?.at(0)?.candidates?.at(0)?.content?.parts) ||
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 +178,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 +197,87 @@ 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?.map((part: { text: string }) => part.text)
.join("\n\n");
},
// processToolMessage, include tool_calls message and tool call results
(
requestPayload: RequestPayload,
toolCallMessage: any,
toolCallResult: any[],
) => {
// @ts-ignore
requestPayload?.contents?.splice(
// @ts-ignore
requestPayload?.contents?.length,
0,
{
role: "model",
parts: toolCallMessage.tool_calls.map(
(tool: ChatMessageTool) => ({
functionCall: {
name: tool?.function?.name,
args: JSON.parse(tool?.function?.arguments as string),
},
}),
),
},
// @ts-ignore
...toolCallResult.map((result) => ({
role: "function",
parts: [
{
functionResponse: {
name: result.name,
response: {
name: result.name,
content: result.content, // TODO just text content...
},
},
},
],
})),
);
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 +292,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,11 +2,9 @@
// 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,
@@ -15,28 +13,18 @@ import {
ChatMessageTool,
usePluginStore,
} from "@/app/store";
import { collectModelsWithDefaultModel } from "@/app/utils/model";
import { preProcessImageContent, stream } from "@/app/utils/chat";
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
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;
@@ -53,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("/")) {
@@ -72,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) {
@@ -188,7 +180,7 @@ export class MoonshotApi 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,7 +2,7 @@
// azure and openai, using same models. so using same LLMApi.
import {
ApiPath,
DEFAULT_API_HOST,
OPENAI_BASE_URL,
DEFAULT_MODELS,
OpenaiPath,
Azure,
@@ -24,7 +24,7 @@ import {
stream,
} from "@/app/utils/chat";
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
import { DalleSize, DalleQuality, DalleStyle } from "@/app/typing";
import { ModelSize, DalleQuality, DalleStyle } from "@/app/typing";
import {
ChatOptions,
@@ -33,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;
@@ -69,6 +65,7 @@ export interface RequestPayload {
frequency_penalty: number;
top_p: number;
max_tokens?: number;
max_completion_tokens?: number;
}
export interface DalleRequestPayload {
@@ -76,7 +73,7 @@ export interface DalleRequestPayload {
prompt: string;
response_format: "url" | "b64_json";
n: number;
size: DalleSize;
size: ModelSize;
quality: DalleQuality;
style: DalleStyle;
}
@@ -103,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("/")) {
@@ -147,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,
@@ -160,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,
@@ -181,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);
}
}
@@ -241,6 +284,7 @@ export class ChatGPTApi implements LLMApi {
);
}
if (shouldStream) {
let index = -1;
const [tools, funcs] = usePluginStore
.getState()
.getAsTools(
@@ -266,10 +310,10 @@ export class ChatGPTApi implements LLMApi {
}>;
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) {
index += 1;
runTools.push({
id,
type: tool_calls[0]?.type,
@@ -291,6 +335,8 @@ export class ChatGPTApi implements LLMApi {
toolCallMessage: any,
toolCallResult: any[],
) => {
// reset index value
index = -1;
// @ts-ignore
requestPayload?.messages?.splice(
// @ts-ignore
@@ -313,7 +359,7 @@ export class ChatGPTApi implements LLMApi {
// make a fetch request
const requestTimeoutId = setTimeout(
() => controller.abort(),
isDalle3 ? REQUEST_TIMEOUT_MS * 2 : REQUEST_TIMEOUT_MS, // dalle3 using b64_json is slow.
isDalle3 || isO1 ? REQUEST_TIMEOUT_MS * 4 : REQUEST_TIMEOUT_MS, // dalle3 using b64_json is slow.
);
const res = await fetch(chatPath, chatPayload);
@@ -321,7 +367,7 @@ export class ChatGPTApi implements LLMApi {
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);
@@ -407,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

@@ -7,7 +7,6 @@ import {
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";
@@ -80,7 +79,7 @@ export const HTMLPreview = forwardRef<HTMLPreviewHander, HTMLPreviewProps>(
}, [props.autoHeight, props.height, iframeHeight]);
const srcDoc = useMemo(() => {
const script = `<script>new ResizeObserver((entries) => parent.postMessage({id: '${frameId}', height: entries[0].target.clientHeight}, '*')).observe(document.body)</script>`;
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);
}

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;
@@ -22,12 +23,16 @@ export function IconButton(props: {
}) {
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}
@@ -40,10 +45,9 @@ export function IconButton(props: {
{props.icon && (
<div
aria-label={props.text || props.title}
className={
styles["icon-button-icon"] +
` ${props.type === "primary" && "no-dark"}`
}
className={clsx(styles["icon-button-icon"], {
"no-dark": props.type === "primary",
})}
>
{props.icon}
</div>

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 {
@@ -373,7 +379,7 @@
}
}
.chat-message-user>.chat-message-container {
.chat-message-user > .chat-message-container {
align-items: flex-end;
}
@@ -443,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;
@@ -471,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;
@@ -497,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);
}
}
@@ -515,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 {
@@ -626,7 +655,8 @@
min-height: 68px;
}
.chat-input:focus {}
.chat-input:focus {
}
.chat-input-send {
background-color: var(--primary);
@@ -646,3 +676,78 @@
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>
@@ -179,7 +179,11 @@ 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 />} />
@@ -197,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>;
@@ -23,7 +24,7 @@ export function InputRange({
aria,
}: InputRangeProps) {
return (
<div className={styles["input-range"] + ` ${className ?? ""}`}>
<div className={clsx(styles["input-range"], className)}>
{title || value}
<input
aria-label={aria}

View File

@@ -22,6 +22,9 @@ import {
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);
@@ -55,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",
@@ -87,12 +90,18 @@ export function PreCode(props: { children: any }) {
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);
const enableArtifacts = session.mask?.enableArtifacts !== false;
const config = useAppConfig();
const enableArtifacts =
session.mask?.enableArtifacts !== false && config.enableArtifacts;
//Wrap the paragraph for plain-text
useEffect(() => {
@@ -128,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>
@@ -164,6 +174,12 @@ export function PreCode(props: { children: any }) {
}
function CustomCode(props: { children: any; className?: string }) {
const chatStore = useChatStore();
const session = chatStore.currentSession();
const config = useAppConfig();
const enableCodeFold =
session.mask?.enableCodeFold !== false && config.enableCodeFold;
const ref = useRef<HTMLPreElement>(null);
const [collapsed, setCollapsed] = useState(true);
const [showToggle, setShowToggle] = useState(false);
@@ -179,46 +195,39 @@ function CustomCode(props: { children: any; className?: string }) {
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={props?.className}
className={clsx(props?.className)}
ref={ref}
style={{
maxHeight: collapsed ? "400px" : "none",
maxHeight: enableCodeFold && collapsed ? "400px" : "none",
overflowY: "hidden",
}}
>
{props.children}
</code>
{showToggle && collapsed && (
<div
className={`show-hide-button ${collapsed ? "collapsed" : "expanded"}`}
>
<button onClick={toggleCollapsed}>{Locale.NewChat.More}</button>
</div>
)}
{renderShowMoreButton()}
</>
);
}
function escapeDollarNumber(text: string) {
let escapedText = "";
for (let i = 0; i < text.length; i += 1) {
let char = text[i];
const nextChar = text[i + 1] || " ";
if (char === "$" && nextChar >= "0" && nextChar <= "9") {
char = "\\$";
}
escapedText += char;
}
return escapedText;
}
function escapeBrackets(text: string) {
const pattern =
/(```[\s\S]*?```|`.*?`)|\\\[([\s\S]*?[^\\])\\\]|\\\((.*?)\\\)/g;
@@ -237,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 (
@@ -261,6 +291,20 @@ function _MarkDownContent(props: { content: string }) {
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[] {
@@ -167,21 +167,40 @@ export function MaskConfig(props: {
></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={props.mask.enableArtifacts !== false}
onChange={(e) => {
props.updateMask((mask) => {
mask.enableArtifacts = e.currentTarget.checked;
});
}}
></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
@@ -426,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()
@@ -542,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);
}
}}
>
@@ -579,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,13 +5,21 @@ 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 (
<>
@@ -19,21 +27,26 @@ export function ModelConfigList(props: {
<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
@@ -228,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

@@ -10,7 +10,29 @@
max-height: 240px;
overflow-y: auto;
white-space: pre-wrap;
min-width: 300px;
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;
}
}
}

View File

@@ -12,7 +12,6 @@ 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 EyeIcon from "../icons/eye.svg";
import ConfirmIcon from "../icons/confirm.svg";
import ReloadIcon from "../icons/reload.svg";
import GithubIcon from "../icons/github.svg";
@@ -28,8 +27,8 @@ import {
} from "./ui-lib";
import Locale from "../locales";
import { useNavigate } from "react-router-dom";
import { useEffect, useState } from "react";
import { getClientConfig } from "../config/client";
import { useState } from "react";
import clsx from "clsx";
export function PluginPage() {
const navigate = useNavigate();
@@ -201,7 +200,7 @@ export function PluginPage() {
<div className={styles["mask-name"]}>
{m.title}@<small>{m.version}</small>
</div>
<div className={styles["mask-info"] + " one-line"}>
<div className={clsx(styles["mask-info"], "one-line")}>
{Locale.Plugin.Item.Info(
FunctionToolService.add(m).length,
)}
@@ -209,19 +208,11 @@ export function PluginPage() {
</div>
</div>
<div className={styles["mask-actions"]}>
{m.builtin ? (
<IconButton
icon={<EyeIcon />}
text={Locale.Plugin.Item.View}
onClick={() => setEditingPluginId(m.id)}
/>
) : (
<IconButton
icon={<EditIcon />}
text={Locale.Plugin.Item.Edit}
onClick={() => setEditingPluginId(m.id)}
/>
)}
<IconButton
icon={<EditIcon />}
text={Locale.Plugin.Item.Edit}
onClick={() => setEditingPluginId(m.id)}
/>
{!m.builtin && (
<IconButton
icon={<DeleteIcon />}
@@ -325,30 +316,13 @@ export function PluginPage() {
></PasswordInput>
</ListItem>
)}
{!getClientConfig()?.isApp && (
<ListItem
title={Locale.Plugin.Auth.Proxy}
subTitle={Locale.Plugin.Auth.ProxyDescription}
>
<input
type="checkbox"
checked={editingPlugin?.usingProxy}
style={{ minWidth: 16 }}
onChange={(e) => {
pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
plugin.usingProxy = e.currentTarget.checked;
});
}}
></input>
</ListItem>
)}
</List>
<List>
<ListItem title={Locale.Plugin.EditModal.Content}>
<div style={{ display: "flex", justifyContent: "flex-end" }}>
<div className={pluginStyles["plugin-schema"]}>
<input
type="text"
style={{ minWidth: 200, marginRight: 20 }}
style={{ minWidth: 200 }}
onInput={(e) => setLoadUrl(e.currentTarget.value)}
></input>
<IconButton
@@ -362,7 +336,10 @@ export function PluginPage() {
<ListItem
subTitle={
<div
className={`markdown-body ${pluginStyles["plugin-content"]}`}
className={clsx(
"markdown-body",
pluginStyles["plugin-content"],
)}
dir="auto"
>
<pre>

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>

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">

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();
@@ -582,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) {
@@ -685,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
@@ -1166,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 && (
<>
@@ -1329,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>}
@@ -1464,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 />
@@ -1540,6 +1692,7 @@ export function Settings() {
</List>
<List id={SlotID.CustomModel}>
{saasStartComponent}
{accessCodeComponent}
{!accessStore.hideUserApiKey && (
@@ -1582,6 +1735,8 @@ export function Settings() {
{moonshotConfigComponent}
{stabilityConfigComponent}
{lflytekConfigComponent}
{XAIConfigComponent}
{chatglmConfigComponent}
</>
)}
</>
@@ -1616,9 +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"
@@ -1645,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";
@@ -23,7 +22,6 @@ import {
MIN_SIDEBAR_WIDTH,
NARROW_SIDEBAR_WIDTH,
Path,
PLUGINS,
REPO_URL,
} from "../constant";
@@ -31,6 +29,13 @@ 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 DISCOVERY = [
{ name: Locale.Plugin.Name, path: Path.Plugins },
{ name: "Stable Diffusion", path: Path.Sd },
{ name: Locale.SearchChat.Page.Title, path: Path.SearchChat },
];
const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, {
loading: () => null,
@@ -142,9 +147,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 +171,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>
@@ -213,7 +224,7 @@ export function SideBarTail(props: {
export function SideBar(props: { className?: string }) {
useHotKey();
const { onDragStart, shouldNarrow } = useDragSideBar();
const [showPluginSelector, setShowPluginSelector] = useState(false);
const [showDiscoverySelector, setshowDiscoverySelector] = useState(false);
const navigate = useNavigate();
const config = useAppConfig();
const chatStore = useChatStore();
@@ -228,6 +239,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
@@ -247,26 +259,21 @@ export function SideBar(props: { className?: string }) {
icon={<DiscoveryIcon />}
text={shouldNarrow ? undefined : Locale.Discovery.Name}
className={styles["sidebar-bar-button"]}
onClick={() => setShowPluginSelector(true)}
onClick={() => setshowDiscoverySelector(true)}
shadow
/>
</div>
{showPluginSelector && (
{showDiscoverySelector && (
<Selector
items={[
{
title: "👇 Please select the plugin you need to use",
value: "-",
disable: true,
},
...PLUGINS.map((item) => {
...DISCOVERY.map((item) => {
return {
title: item.name,
value: item.path,
};
}),
]}
onClose={() => setShowPluginSelector(false)}
onClose={() => setshowDiscoverySelector(false)}
onSelection={(s) => {
navigate(s[0], { state: { fromHome: true } });
}}
@@ -285,7 +292,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 () => {

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,7 +46,7 @@ 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>
);
}
@@ -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,7 +263,7 @@ export function Input(props: InputProps) {
return (
<textarea
{...props}
className={`${styles["input"]} ${props.className}`}
className={clsx(styles["input"], props.className)}
></textarea>
);
}
@@ -292,13 +295,23 @@ export function PasswordInput(
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>
@@ -503,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,6 @@
import md5 from "spark-md5";
import { DEFAULT_MODELS, DEFAULT_GA_ID } from "../constant";
import { isGPT4Model } from "../utils/model";
declare global {
namespace NodeJS {
@@ -71,6 +72,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,16 +128,12 @@ export const getServerSideConfig = () => {
if (disableGPT4) {
if (customModels) customModels += ",";
customModels += DEFAULT_MODELS.filter(
(m) => m.name.startsWith("gpt-4") && !m.name.startsWith("gpt-4o-mini"),
)
customModels += DEFAULT_MODELS.filter((m) => isGPT4Model(m.name))
.map((m) => "-" + m.name)
.join(",");
if (
defaultModel.startsWith("gpt-4") &&
!defaultModel.startsWith("gpt-4o-mini")
)
if (defaultModel && isGPT4Model(defaultModel)) {
defaultModel = "";
}
}
const isStability = !!process.env.STABILITY_API_KEY;
@@ -143,6 +148,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);
@@ -151,8 +158,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 {
@@ -205,6 +212,14 @@ 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),
@@ -226,6 +241,6 @@ export const getServerSideConfig = () => {
disableFastLink: !!process.env.DISABLE_FAST_LINK,
customModels,
defaultModel,
allowedWebDevEndpoints,
allowedWebDavEndpoints,
};
};

View File

@@ -1,5 +1,3 @@
import path from "path";
export const OWNER = "ChatGPTNextWeb";
export const REPO = "ChatGPT-Next-Web";
export const REPO_URL = `https://github.com/${OWNER}/${REPO}`;
@@ -13,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";
@@ -31,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`;
@@ -62,6 +63,8 @@ export enum ApiPath {
Iflytek = "/api/iflytek",
Stability = "/api/stability",
Artifacts = "/api/artifacts",
XAI = "/api/xai",
ChatGLM = "/api/chatglm",
}
export enum SlotID {
@@ -114,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
@@ -136,6 +141,8 @@ export enum ModelProvider {
Hunyuan = "Hunyuan",
Moonshot = "Moonshot",
Iflytek = "Iflytek",
XAI = "XAI",
ChatGLM = "ChatGLM",
}
export const Stability = {
@@ -152,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",
@@ -217,6 +225,18 @@ 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",
ImagePath: "api/paas/v4/images/generations",
VideoPath: "api/paas/v4/videos/generations",
};
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}}.
@@ -246,15 +266,52 @@ export const KnowledgeCutOffDate: Record<string, string> = {
"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"
/glm-4v-plus/,
/glm-4v/,
/glm-4v-flash/,
];
export const EXCLUDE_VISION_MODEL_REGEXES = [/claude-3-5-haiku-20241022/];
const openaiModels = [
"gpt-3.5-turbo",
"gpt-3.5-turbo-1106",
@@ -268,19 +325,36 @@ const openaiModels = [
"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 = [
@@ -289,8 +363,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 = [
@@ -346,6 +425,28 @@ 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",
"glm-4v-plus",
"glm-4v",
"glm-4v-flash", // free
"cogview-3-plus",
"cogview-3",
"cogview-3-flash", // free
// 目前无法适配轮询任务
// "cogvideox",
// "cogvideox-flash", // free
];
let seq = 1000; // 内置的模型序号生成器从1000开始
export const DEFAULT_MODELS = [
...openaiModels.map((name) => ({
@@ -458,6 +559,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;
@@ -477,8 +600,6 @@ export const internalAllowedWebDavEndpoints = [
];
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";

7
app/global.d.ts vendored
View File

@@ -26,6 +26,13 @@ declare interface Window {
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,

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

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

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

View File

@@ -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" crossOrigin="use-credentials"></link>
<link
rel="manifest"
href="/site.webmanifest"
crossOrigin="use-credentials"
></link>
<script src="/serviceWorkerRegister.js" defer></script>
</head>
<body>

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,15 +1,21 @@
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: isApp
? "تم اكتشاف مفتاح API غير صالح، يرجى الذهاب إلى [الإعدادات](/#/settings) للتحقق من صحة مفتاح API."
: "كلمة المرور غير صحيحة أو فارغة، يرجى الذهاب إلى [تسجيل الدخول](/#/auth) لإدخال كلمة مرور صحيحة، أو أدخل مفتاح OpenAI API الخاص بك في [الإعدادات](/#/settings).",
? `😆 واجهت المحادثة بعض المشكلات، لا داعي للقلق:
\\ 1⃣ إذا كنت ترغب في تجربة دون إعداد، [انقر هنا لبدء المحادثة فورًا 🚀](${SAAS_CHAT_UTM_URL})
\\ 2⃣ إذا كنت تريد استخدام موارد OpenAI الخاصة بك، انقر [هنا](/#/settings) لتعديل الإعدادات ⚙️`
: `😆 واجهت المحادثة بعض المشكلات، لا داعي للقلق:
\ 1⃣ إذا كنت ترغب في تجربة دون إعداد، [انقر هنا لبدء المحادثة فورًا 🚀](${SAAS_CHAT_UTM_URL})
\ 2⃣ إذا كنت تستخدم إصدار النشر الخاص، انقر [هنا](/#/auth) لإدخال مفتاح الوصول 🔑
\ 3⃣ إذا كنت تريد استخدام موارد OpenAI الخاصة بك، انقر [هنا](/#/settings) لتعديل الإعدادات ⚙️
`,
},
Auth: {
Title: "تحتاج إلى كلمة مرور",
@@ -18,6 +24,10 @@ const ar: PartialLocaleType = {
Input: "أدخل رمز الوصول هنا",
Confirm: "تأكيد",
Later: "في وقت لاحق",
Return: "عودة",
SaasTips: "الإعدادات معقدة، أريد استخدامه على الفور",
TopTips:
"🥳 عرض NextChat AI الأول، افتح الآن OpenAI o1, GPT-4o, Claude-3.5 وأحدث النماذج الكبيرة",
},
ChatItem: {
ChatItemCount: (count: number) => `${count} محادثة`,
@@ -43,6 +53,8 @@ const ar: PartialLocaleType = {
PinToastAction: "عرض",
Delete: "حذف",
Edit: "تحرير",
RefreshTitle: "تحديث العنوان",
RefreshToast: "تم إرسال طلب تحديث العنوان",
},
Commands: {
new: "دردشة جديدة",
@@ -279,6 +291,13 @@ const ar: PartialLocaleType = {
},
Access: {
SaasStart: {
Title: "استخدام NextChat AI",
Label: "(أفضل حل من حيث التكلفة)",
SubTitle:
"مدعوم رسميًا من NextChat، جاهز للاستخدام بدون إعداد، يدعم أحدث النماذج الكبيرة مثل OpenAI o1 و GPT-4o و Claude-3.5",
ChatNow: "الدردشة الآن",
},
AccessCode: {
Title: "كلمة المرور للوصول",
SubTitle: "قام المشرف بتمكين الوصول المشفر",
@@ -404,6 +423,10 @@ const ar: PartialLocaleType = {
},
Model: "النموذج",
CompressModel: {
Title: "نموذج الضغط",
SubTitle: "النموذج المستخدم لضغط السجل التاريخي",
},
Temperature: {
Title: "العشوائية (temperature)",
SubTitle: "كلما زادت القيمة، زادت العشوائية في الردود",

View File

@@ -1,15 +1,21 @@
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 bn: PartialLocaleType = {
WIP: "শীঘ্রই আসছে...",
Error: {
Unauthorized: isApp
? "অবৈধ API কী সনাক্ত করা হয়েছে, অনুগ্রহ করে [সেটিংস](/#/settings) পৃষ্ঠায় যান এবং নিশ্চিত করুন যে API কী সঠিকভাবে কনফিগার করা হয়েছে।"
: "অ্যাক্সেস পাসওয়ার্ড সঠিক নয় বা খালি, অনুগ্রহ করে [লগইন](/#/auth) পৃষ্ঠায় যান এবং সঠিক অ্যাক্সেস পাসওয়ার্ড প্রবেরান, অথবা [সেটিংস](/#/settings) পৃষ্ঠায় আপনার OpenAI API কী প্রবেশ করান।",
? `😆 কথোপকথনে কিছু সমস্যা হয়েছে, চিন্তার কিছু নেই:
\\ 1⃣ যদি আপনি শূন্য কনফিগারেশনে শুরু করচান, তাহলে [এখানে ক্লিক করে অবিলম্বে কথোপকথন শুরু করুন 🚀](${SAAS_CHAT_UTM_URL})
\\ 2⃣ যদি আপনি আপনার নিজস্ব OpenAI সম্পদ ব্যবহার করতে চান, তাহলে [এখানে ক্লিক করুন](/#/settings) সেটিংস পরিবর্তন করতে ⚙️`
: `😆 কথোপকথনে কিছু সমস্যা হয়েছে, চিন্তার কিছু নেই:
\ 1⃣ যদি আপনি শূন্য কনফিগারেশনে শুরু করতে চান, তাহলে [এখানে ক্লিক করে অবিলম্বে কথোপকথন শুরু করুন 🚀](${SAAS_CHAT_UTM_URL})
\ 2⃣ যদি আপনি একটি প্রাইভেট ডেপ্লয়মেন্ট সংস্করণ ব্যবহার করেন, তাহলে [এখানে ক্লিক করুন](/#/auth) প্রবেশাধিকার কীগুলি প্রবেশ করতে 🔑
\ 3⃣ যদি আপনি আপনার নিজস্ব OpenAI সম্পদ ব্যবহার করতে চান, তাহলে [এখানে ক্লিক করুন](/#/settings) সেটিংস পরিবর্তন করতে ⚙️
`,
},
Auth: {
Title: "পাসওয়ার্ড প্রয়োজন",
@@ -18,6 +24,10 @@ const bn: PartialLocaleType = {
Input: "এখানে অ্যাক্সেস কোড লিখুন",
Confirm: "নিশ্চিত করুন",
Later: "পরে বলুন",
Return: "ফিরে আসা",
SaasTips: "কনফিগারেশন খুব কঠিন, আমি অবিলম্বে ব্যবহার করতে চাই",
TopTips:
"🥳 NextChat AI প্রথম প্রকাশের অফার, এখনই OpenAI o1, GPT-4o, Claude-3.5 এবং সর্বশেষ বড় মডেলগুলি আনলক করুন",
},
ChatItem: {
ChatItemCount: (count: number) => `${count} টি চ্যাট`,
@@ -43,6 +53,8 @@ const bn: PartialLocaleType = {
PinToastAction: "দেখুন",
Delete: "মুছে ফেলুন",
Edit: "সম্পাদনা করুন",
RefreshTitle: "শিরোনাম রিফ্রেশ করুন",
RefreshToast: "শিরোনাম রিফ্রেশ অনুরোধ পাঠানো হয়েছে",
},
Commands: {
new: "নতুন চ্যাট",
@@ -282,6 +294,14 @@ const bn: PartialLocaleType = {
},
Access: {
SaasStart: {
Title: "NextChat AI ব্যবহার করুন",
Label: "(সেরা মূল্যসাশ্রয়ী সমাধান)",
SubTitle:
"NextChat কর্তৃক অফিসিয়াল রক্ষণাবেক্ষণ, শূন্য কনফিগারেশন ব্যবহার শুরু করুন, OpenAI o1, GPT-4o, Claude-3.5 সহ সর্বশেষ বড় মডেলগুলি সমর্থন করে",
ChatNow: "এখনই চ্যাট করুন",
},
AccessCode: {
Title: "অ্যাক্সেস পাসওয়ার্ড",
SubTitle: "অ্যাডমিন এনক্রিপ্টেড অ্যাক্সেস সক্রিয় করেছেন",
@@ -411,6 +431,10 @@ const bn: PartialLocaleType = {
},
Model: "মডেল (model)",
CompressModel: {
Title: "সংকোচন মডেল",
SubTitle: "ইতিহাস সংকুচিত করার জন্য ব্যবহৃত মডেল",
},
Temperature: {
Title: "যাদুকরিতা (temperature)",
SubTitle: "মান বাড়ালে উত্তর বেশি এলোমেলো হবে",

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} 条对话`,
@@ -43,6 +54,10 @@ const cn = {
Delete: "删除",
Edit: "编辑",
FullScreen: "全屏",
RefreshTitle: "刷新标题",
RefreshToast: "已发送刷新标题请求",
Speech: "朗读",
StopSpeech: "停止",
},
Commands: {
new: "新建聊天",
@@ -50,6 +65,7 @@ const cn = {
next: "下一个聊天",
prev: "上一个聊天",
clear: "清除上下文",
fork: "复制聊天",
del: "删除聊天",
},
InputActions: {
@@ -76,11 +92,21 @@ const cn = {
return inputHints + "/ 触发补全,: 触发命令";
},
Send: "发送",
StartSpeak: "说话",
StopSpeak: "停止",
Config: {
Reset: "清除记忆",
SaveAs: "存为面具",
},
IsContext: "预设提示词",
ShortcutKey: {
Title: "键盘快捷方式",
newChat: "打开新聊天",
focusInput: "聚焦输入框",
copyLastMessage: "复制最后一个回复",
copyLastCode: "复制最后一个代码块",
showShortcutKey: "显示快捷方式",
},
},
Export: {
Title: "分享聊天记录",
@@ -150,7 +176,7 @@ const cn = {
},
},
Lang: {
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
Name: "Language", // 注意:如果要添加新的翻译,请不要翻译此值,将它保留为 `Language`
All: "所有语言",
},
Avatar: "头像",
@@ -179,6 +205,8 @@ const cn = {
IsChecking: "正在检查更新...",
FoundUpdate: (x: string) => `发现新版本:${x}`,
GoToUpdate: "前往更新",
Success: "更新成功!",
Failed: "更新失败",
},
SendKey: "发送键",
Theme: "主题",
@@ -282,6 +310,13 @@ const cn = {
},
Access: {
SaasStart: {
Title: "使用 NextChat AI",
Label: "(性价比最高的方案)",
SubTitle:
"由 NextChat 官方维护, 零配置开箱即用,支持 OpenAI o1, GPT-4o, Claude-3.5 等最新大模型",
ChatNow: "立刻对话",
},
AccessCode: {
Title: "访问密码",
SubTitle: "管理员已开启加密访问",
@@ -345,7 +380,7 @@ const cn = {
ApiKey: {
Title: "API 密钥",
SubTitle: "从 Google AI 获取您的 API 密钥",
Placeholder: "输入您的 Google AI Studio API 密钥",
Placeholder: "Google AI API KEY",
},
Endpoint: {
@@ -427,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: "接口密钥",
@@ -461,6 +518,10 @@ const cn = {
},
Model: "模型 (model)",
CompressModel: {
Title: "对话摘要模型",
SubTitle: "用于压缩历史记录、生成对话标题的模型",
},
Temperature: {
Title: "随机性 (temperature)",
SubTitle: "值越大,回复越随机",
@@ -481,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: "新的聊天",
@@ -495,8 +609,8 @@ const cn = {
},
},
Copy: {
Success: "已写入剪板",
Failed: "复制失败,请赋予剪板权限",
Success: "已写入剪板",
Failed: "复制失败,请赋予剪板权限",
},
Download: {
Success: "内容已下载到您的目录。",
@@ -516,7 +630,7 @@ const cn = {
Sysmessage: "你是一个助手",
},
SearchChat: {
Name: "搜索",
Name: "搜索聊天记录",
Page: {
Title: "搜索聊天记录",
Search: "输入搜索关键词",
@@ -608,6 +722,10 @@ const cn = {
Title: "启用Artifacts",
SubTitle: "启用之后可以直接渲染HTML页面",
},
CodeFold: {
Title: "启用代码折叠",
SubTitle: "启用之后可以自动折叠/展开过长的代码块",
},
Share: {
Title: "分享此面具",
SubTitle: "生成此面具的直达链接",

View File

@@ -1,15 +1,21 @@
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: isApp
? "Byl zjištěn neplatný API Key, prosím přejděte na stránku [Nastavení](/#/settings) a zkontrolujte, zda je API Key správně nakonfigurován."
: "Heslo je nesprávné nebo prázdné, prosím přejděte na stránku [Přihlášení](/#/auth) a zadejte správné heslo, nebo na stránku [Nastavení](/#/settings) a zadejte svůj vlastní OpenAI API Key.",
? `😆 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",
@@ -18,6 +24,10 @@ const cs: PartialLocaleType = {
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} konverzací`,
@@ -43,6 +53,8 @@ const cs: PartialLocaleType = {
PinToastAction: "Zobrazit",
Delete: "Smazat",
Edit: "Upravit",
RefreshTitle: "Obnovit název",
RefreshToast: "Požadavek na obnovení názvu byl odeslán",
},
Commands: {
new: "Nová konverzace",
@@ -282,6 +294,14 @@ const cs: PartialLocaleType = {
},
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",
@@ -410,6 +430,10 @@ const cs: PartialLocaleType = {
},
Model: "Model (model)",
CompressModel: {
Title: "Kompresní model",
SubTitle: "Model používaný pro kompresi historie",
},
Temperature: {
Title: "Náhodnost (temperature)",
SubTitle: "Čím vyšší hodnota, tím náhodnější odpovědi",

View File

@@ -1,15 +1,21 @@
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: isApp
? "Ungültiger API-Schlüssel erkannt. Bitte gehen Sie zur [Einstellungen](/#/settings) Seite, um zu überprüfen, ob der API-Schlüssel korrekt konfiguriert ist."
: "Das Passwort ist falsch oder leer. Bitte gehen Sie zur [Login](/#/auth) Seite, um das richtige Passwort einzugeben, oder fügen Sie Ihren OpenAI API-Schlüssel auf der [Einstellungen](/#/settings) Seite hinzu.",
? `😆 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",
@@ -18,6 +24,11 @@ const de: PartialLocaleType = {
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} Gespräche`,
@@ -43,6 +54,8 @@ const de: PartialLocaleType = {
PinToastAction: "Ansehen",
Delete: "Löschen",
Edit: "Bearbeiten",
RefreshTitle: "Titel aktualisieren",
RefreshToast: "Anfrage zur Titelaktualisierung gesendet",
},
Commands: {
new: "Neues Gespräch",
@@ -289,6 +302,14 @@ const de: PartialLocaleType = {
},
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:
@@ -421,6 +442,10 @@ const de: PartialLocaleType = {
},
Model: "Modell",
CompressModel: {
Title: "Kompressionsmodell",
SubTitle: "Modell zur Komprimierung des Verlaufs",
},
Temperature: {
Title: "Zufälligkeit (temperature)",
SubTitle: "Je höher der Wert, desto zufälliger die Antwort",

View File

@@ -1,7 +1,7 @@
import { getClientConfig } from "../config/client";
import { SubmitKey } from "../store/config";
import { LocaleType } from "./index";
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
// if you are adding a new translation, please use PartialLocaleType instead of LocaleType
const isApp = !!getClientConfig()?.isApp;
@@ -9,16 +9,26 @@ const en: LocaleType = {
WIP: "Coming Soon...",
Error: {
Unauthorized: isApp
? "Invalid API Key, please check it in [Settings](/#/settings) page."
: "Unauthorized access, please enter access code in [auth](/#/auth) page, or enter your OpenAI API Key.",
? `😆 Oops, there's an issue. No worries:
\\ 1⃣ New here? [Click to start chatting now 🚀](${SAAS_CHAT_UTM_URL})
\\ 2⃣ Want to use your own OpenAI resources? [Click here](/#/settings) to change settings ⚙️`
: `😆 Oops, there's an issue. Let's fix it:
\ 1⃣ New here? [Click to start chatting now 🚀](${SAAS_CHAT_UTM_URL})
\ 2⃣ Using a private setup? [Click here](/#/auth) to enter your key 🔑
\ 3⃣ Want to use your own OpenAI resources? [Click here](/#/settings) to change settings ⚙️
`,
},
Auth: {
Return: "Return",
Title: "Need Access Code",
Tips: "Please enter access code below",
SubTips: "Or enter your OpenAI or Google API Key",
Input: "access code",
Confirm: "Confirm",
Later: "Later",
SaasTips: "Too Complex, Use Immediately Now",
TopTips:
"🥳 NextChat AI launch promotion: Instantly unlock the latest models like OpenAI o1, GPT-4o, Claude-3.5!",
},
ChatItem: {
ChatItemCount: (count: number) => `${count} messages`,
@@ -45,6 +55,10 @@ const en: LocaleType = {
Delete: "Delete",
Edit: "Edit",
FullScreen: "FullScreen",
RefreshTitle: "Refresh Title",
RefreshToast: "Title refresh request sent",
Speech: "Play",
StopSpeech: "Stop",
},
Commands: {
new: "Start a new chat",
@@ -52,6 +66,7 @@ const en: LocaleType = {
next: "Next Chat",
prev: "Previous Chat",
clear: "Clear Context",
fork: "Copy Chat",
del: "Delete Chat",
},
InputActions: {
@@ -78,11 +93,21 @@ const en: LocaleType = {
return inputHints + ", / to search prompts, : to use commands";
},
Send: "Send",
StartSpeak: "Start Speak",
StopSpeak: "Stop Speak",
Config: {
Reset: "Reset to Default",
SaveAs: "Save as Mask",
},
IsContext: "Contextual Prompt",
ShortcutKey: {
Title: "Keyboard Shortcuts",
newChat: "Open New Chat",
focusInput: "Focus Input Field",
copyLastMessage: "Copy Last Reply",
copyLastCode: "Copy Last Code Block",
showShortcutKey: "Show Shortcuts",
},
},
Export: {
Title: "Export Messages",
@@ -182,6 +207,8 @@ const en: LocaleType = {
IsChecking: "Checking update...",
FoundUpdate: (x: string) => `Found new version: ${x}`,
GoToUpdate: "Update",
Success: "Update Successful.",
Failed: "Update Failed.",
},
SendKey: "Send Key",
Theme: "Theme",
@@ -286,6 +313,14 @@ const en: LocaleType = {
NoAccess: "Enter API Key to check balance",
},
Access: {
SaasStart: {
Title: "Use NextChat AI",
Label: " (Most Cost-Effective Option)",
SubTitle:
"Maintained by NextChat, zero setup needed, unlock OpenAI o1, GPT-4o," +
" Claude-3.5 and more",
ChatNow: "Start Now",
},
AccessCode: {
Title: "Access Code",
SubTitle: "Access control Enabled",
@@ -411,6 +446,28 @@ const en: LocaleType = {
SubTitle: "Example: ",
},
},
XAI: {
ApiKey: {
Title: "XAI API Key",
SubTitle: "Use a custom XAI API Key",
Placeholder: "XAI API Key",
},
Endpoint: {
Title: "Endpoint Address",
SubTitle: "Example: ",
},
},
ChatGLM: {
ApiKey: {
Title: "ChatGLM API Key",
SubTitle: "Use a custom ChatGLM API Key",
Placeholder: "ChatGLM API Key",
},
Endpoint: {
Title: "Endpoint Address",
SubTitle: "Example: ",
},
},
Stability: {
ApiKey: {
Title: "Stability API Key",
@@ -446,7 +503,7 @@ const en: LocaleType = {
ApiKey: {
Title: "API Key",
SubTitle: "Obtain your API Key from Google AI",
Placeholder: "Enter your Google AI Studio API Key",
Placeholder: "Google AI API Key",
},
Endpoint: {
@@ -466,6 +523,10 @@ const en: LocaleType = {
},
Model: "Model",
CompressModel: {
Title: "Summary Model",
SubTitle: "Model used to compress history and generate title",
},
Temperature: {
Title: "Temperature",
SubTitle: "A larger value makes the more random output",
@@ -488,6 +549,60 @@ const en: LocaleType = {
SubTitle:
"A larger value decreasing the likelihood to repeat the same line",
},
TTS: {
Enable: {
Title: "Enable TTS",
SubTitle: "Enable text-to-speech service",
},
Autoplay: {
Title: "Enable Autoplay",
SubTitle:
"Automatically generate speech and play, you need to enable the text-to-speech switch first",
},
Model: "Model",
Voice: {
Title: "Voice",
SubTitle: "The voice to use when generating the audio",
},
Speed: {
Title: "Speed",
SubTitle: "The speed of the generated audio",
},
Engine: "TTS Engine",
},
Realtime: {
Enable: {
Title: "Realtime Chat",
SubTitle: "Enable realtime chat feature",
},
Provider: {
Title: "Model Provider",
SubTitle: "Switch between different providers",
},
Model: {
Title: "Model",
SubTitle: "Select a model",
},
ApiKey: {
Title: "API Key",
SubTitle: "API Key",
Placeholder: "API Key",
},
Azure: {
Endpoint: {
Title: "Endpoint",
SubTitle: "Endpoint",
},
Deployment: {
Title: "Deployment Name",
SubTitle: "Deployment Name",
},
},
Temperature: {
Title: "Randomness (temperature)",
SubTitle: "Higher values result in more random responses",
},
},
},
Store: {
DefaultTopic: "New Conversation",
@@ -617,6 +732,11 @@ const en: LocaleType = {
Title: "Enable Artifacts",
SubTitle: "Can render HTML page when enable artifacts.",
},
CodeFold: {
Title: "Enable CodeFold",
SubTitle:
"Automatically collapse/expand overly long code blocks when CodeFold is enabled",
},
Share: {
Title: "Share This Mask",
SubTitle: "Generate a link to this mask",

View File

@@ -1,15 +1,21 @@
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 es: PartialLocaleType = {
WIP: "En construcción...",
Error: {
Unauthorized: isApp
? "Se detectó una clave API inválida. Por favor, ve a la página de [Configuración](/#/settings) para verificar si la clave API está configurada correctamente."
: "La contraseña de acceso es incorrecta o está vacía. Por favor, ve a la página de [Iniciar sesión](/#/auth) para ingresar la contraseña correcta, o en la página de [Configuración](/#/settings) para introducir tu propia clave API de OpenAI.",
? `😆 La conversación encontró algunos problemas, no te preocupes:
\\ 1⃣ Si deseas comenzar sin configuración, [haz clic aquí para empezar a chatear inmediatamente 🚀](${SAAS_CHAT_UTM_URL})
\\ 2⃣ Si deseas usar tus propios recursos de OpenAI, haz clic [aquí](/#/settings) para modificar la configuración ⚙️`
: `😆 La conversación encontró algunos problemas, no te preocupes:
\ 1⃣ Si deseas comenzar sin configuración, [haz clic aquí para empezar a chatear inmediatamente 🚀](${SAAS_CHAT_UTM_URL})
\ 2⃣ Si estás utilizando una versión de implementación privada, haz clic [aquí](/#/auth) para ingresar la clave de acceso 🔑
\ 3⃣ Si deseas usar tus propios recursos de OpenAI, haz clic [aquí](/#/settings) para modificar la configuración ⚙️
`,
},
Auth: {
Title: "Se requiere contraseña",
@@ -18,6 +24,11 @@ const es: PartialLocaleType = {
Input: "Introduce el código de acceso aquí",
Confirm: "Confirmar",
Later: "Más tarde",
Return: "Regresar",
SaasTips:
"La configuración es demasiado complicada, quiero usarlo de inmediato",
TopTips:
"🥳 Oferta de lanzamiento de NextChat AI, desbloquea OpenAI o1, GPT-4o, Claude-3.5 y los últimos grandes modelos",
},
ChatItem: {
ChatItemCount: (count: number) => `${count} conversaciones`,
@@ -44,6 +55,8 @@ const es: PartialLocaleType = {
PinToastAction: "Ver",
Delete: "Eliminar",
Edit: "Editar",
RefreshTitle: "Actualizar título",
RefreshToast: "Se ha enviado la solicitud de actualización del título",
},
Commands: {
new: "Nueva conversación",
@@ -292,6 +305,14 @@ const es: PartialLocaleType = {
},
Access: {
SaasStart: {
Title: "Use NextChat AI",
Label: "(The most cost-effective solution)",
SubTitle:
"Officially maintained by NextChat, zero configuration ready to use, supports the latest large models like OpenAI o1, GPT-4o, and Claude-3.5",
ChatNow: "Chat Now",
},
AccessCode: {
Title: "Contraseña de acceso",
SubTitle: "El administrador ha habilitado el acceso encriptado",
@@ -423,6 +444,10 @@ const es: PartialLocaleType = {
},
Model: "Modelo (model)",
CompressModel: {
Title: "Modelo de compresión",
SubTitle: "Modelo utilizado para comprimir el historial",
},
Temperature: {
Title: "Aleatoriedad (temperature)",
SubTitle: "Cuanto mayor sea el valor, más aleatorio será el resultado",

View File

@@ -1,15 +1,21 @@
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 fr: PartialLocaleType = {
WIP: "Prochainement...",
Error: {
Unauthorized: isApp
? "Clé API invalide détectée. Veuillez vérifier si la clé API est correctement configurée dans la page [Paramètres](/#/settings)."
: "Le mot de passe d'accès est incorrect ou manquant. Veuillez entrer le mot de passe d'accès correct sur la page [Connexion](/#/auth) ou entrer votre propre clé API OpenAI sur la page [Paramètres](/#/settings).",
? `😆 La conversation a rencontré quelques problèmes, pas de panique :
\\ 1⃣ Si vous souhaitez commencer sans configuration, [cliquez ici pour démarrer la conversation immédiatement 🚀](${SAAS_CHAT_UTM_URL})
\\ 2⃣ Si vous souhaitez utiliser vos propres ressources OpenAI, cliquez [ici](/#/settings) pour modifier les paramètres ⚙️`
: `😆 La conversation a rencontré quelques problèmes, pas de panique :
\ 1⃣ Si vous souhaitez commencer sans configuration, [cliquez ici pour démarrer la conversation immédiatement 🚀](${SAAS_CHAT_UTM_URL})
\ 2⃣ Si vous utilisez une version déployée privée, cliquez [ici](/#/auth) pour entrer la clé d'accès 🔑
\ 3⃣ Si vous souhaitez utiliser vos propres ressources OpenAI, cliquez [ici](/#/settings) pour modifier les paramètres ⚙️
`,
},
Auth: {
Title: "Mot de passe requis",
@@ -18,6 +24,11 @@ const fr: PartialLocaleType = {
Input: "Entrez le code d'accès ici",
Confirm: "Confirmer",
Later: "Plus tard",
Return: "Retour",
SaasTips:
"La configuration est trop compliquée, je veux l'utiliser immédiatement",
TopTips:
"🥳 Offre de lancement NextChat AI, débloquez OpenAI o1, GPT-4o, Claude-3.5 et les derniers grands modèles",
},
ChatItem: {
ChatItemCount: (count: number) => `${count} conversations`,
@@ -43,6 +54,8 @@ const fr: PartialLocaleType = {
PinToastAction: "Voir",
Delete: "Supprimer",
Edit: "Modifier",
RefreshTitle: "Actualiser le titre",
RefreshToast: "Demande d'actualisation du titre envoyée",
},
Commands: {
new: "Nouvelle discussion",
@@ -292,6 +305,14 @@ const fr: PartialLocaleType = {
},
Access: {
SaasStart: {
Title: "Utiliser NextChat AI",
Label: "(La solution la plus rentable)",
SubTitle:
"Officiellement maintenu par NextChat, prêt à l'emploi sans configuration, prend en charge les derniers grands modèles comme OpenAI o1, GPT-4o et Claude-3.5",
ChatNow: "Discuter maintenant",
},
AccessCode: {
Title: "Mot de passe d'accès",
SubTitle: "L'administrateur a activé l'accès sécurisé",
@@ -422,6 +443,10 @@ const fr: PartialLocaleType = {
},
Model: "Modèle",
CompressModel: {
Title: "Modèle de compression",
SubTitle: "Modèle utilisé pour compresser l'historique",
},
Temperature: {
Title: "Aléatoire (temperature)",
SubTitle: "Plus la valeur est élevée, plus les réponses sont aléatoires",

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