Compare commits

...

976 Commits

Author SHA1 Message Date
Fred
0c53579996 refactor: init switching to nextjs router 2024-05-10 14:57:55 +08:00
Fred
00b1a9781d Merge branch 'feat-redesign-ui' into v3 2024-05-08 14:24:43 +08:00
Dean-YZG
240d330001 feat: 1)add font source 2)add validator in ListItem 3)settings page ui optiminize 2024-05-07 15:05:29 +08:00
Dean-YZG
4e4431339f feat: old page ui style optiminize 2024-05-07 11:22:17 +08:00
Dean-YZG
fa2f8c66d1 feat: old page dark mode compatible 2024-05-06 22:23:21 +08:00
Fred
32f62d70af feat: bump version 2024-05-06 14:15:27 +08:00
butterfly
68f0fa917f complete colors in dark mode 2024-05-01 22:35:23 +08:00
butterfly
8a14cb19a9 feat: merge remote 2024-04-30 19:19:27 +08:00
butterfly
3d99965a8f feat: bugfix 2024-04-30 19:11:59 +08:00
Fred
4d5a9476b6 style: add transition 2024-04-30 19:04:17 +08:00
Fred
15d6ed252f style: add transition 2024-04-30 19:02:18 +08:00
Fred
ecf6cc27d6 style: add transition 2024-04-30 18:58:19 +08:00
Fred
cadd2558fd chore: update settings width 2024-04-30 17:42:59 +08:00
butterfly
c3d91bf0cd feat: complete the missing UI 2024-04-30 12:37:28 +08:00
butterfly
996537d262 feat: optiminize modal UE on mobile dev 2024-04-30 11:03:37 +08:00
butterfly
5ea6206319 feat: select model done 2024-04-29 20:37:27 +08:00
butterfly
8c28c408d8 feat: refactor select model 2024-04-29 16:29:47 +08:00
butterfly
c34b8ab919 feat: ui optiminize 2024-04-28 19:58:59 +08:00
butterfly
9f4813326c feat: HoverPopover done 2024-04-28 14:10:58 +08:00
butterfly
9569888b0e feat: ui fixed 2024-04-28 12:49:06 +08:00
butterfly
1a636b0f50 feat: function delete chat dev done 2024-04-26 19:33:22 +08:00
butterfly
48e8c0a194 feat: optiminize 2024-04-26 01:31:03 +08:00
butterfly
59583e53bd feat: light theme mode 2024-04-25 21:57:50 +08:00
butterfly
bb7422c526 Merge remote-tracking branch 'origin/main' into feat-redesign-ui 2024-04-25 11:02:12 +08:00
DeanYao
9aec3b714e Merge pull request #4545 from jalr4ever/main-default-model-env
Support a way to define default model by adding DEFAULT_MODEL env.
2024-04-25 10:58:14 +08:00
butterfly
c99086447e feat: redesign settings page 2024-04-24 15:44:24 +08:00
butterfly
f7074bba8c feat: chat panel header add zindex config 2024-04-22 11:31:53 +08:00
butterfly
4400392c0c feat: chat panel header background blur&transparent 2024-04-22 11:29:20 +08:00
butterfly
4a5465f884 feat: chat panel header absolute 2024-04-22 11:02:25 +08:00
butterfly
37cc87531c feat: optiminize message&img display 2024-04-19 19:28:48 +08:00
Wayland Zhan
c96e4b7966 feat: Support a way to define default model by adding DEFAULT_MODEL env. 2024-04-19 06:57:15 +00:00
butterfly
1074fffe79 feat: clear trash 2024-04-19 14:50:11 +08:00
butterfly
3d0a98d5d2 feat: maskpage&newchatpage adapt new ui framework done 2024-04-19 11:55:51 +08:00
butterfly
b3559f99a2 feat: chat panel UE done 2024-04-18 12:27:44 +08:00
DeanYao
9b2cb1e1c3 Merge pull request #4525 from ChatGPTNextWeb/chore-fix
Chore fix
2024-04-16 14:59:22 +08:00
butterfly
fb8b8d28da feat: (1) fix issues/4335 and issues/4518 2024-04-16 14:50:48 +08:00
butterfly
51a1d9f92a feat: chat panel redesigned ui 2024-04-16 14:07:51 +08:00
DeanYao
ad80153bbb Merge pull request #4520 from Algorithm5838/refactor-models
Refactor DEFAULT_MODELS for better maintainability
2024-04-16 09:33:00 +08:00
Algorithm5838
9564b261d5 Update constant.ts 2024-04-15 13:14:14 +03:00
DeanYao
1e2a662fa6 Merge pull request #4412 from RubuJam/main
Gemini will generate the request address based on the selected model name and supports Gemini 1.5 Pro (gemini-1.5-pro-latest).
2024-04-15 11:44:53 +08:00
DeanYao
51f7daaeaf Merge pull request #4514 from SukkaW/fix-ls-performance
perf: avoid read localStorage on every render
2024-04-15 10:11:03 +08:00
DeanYao
f742a7ec4e Merge pull request #4510 from MrrDrr/add_timezone_in_system_prompts
add timezone in system prompts
2024-04-15 10:09:53 +08:00
DeanYao
e2c0d2a07b Merge pull request #4509 from MrrDrr/add_knowledge_cutoff
add knowledge cutoff date for gpt-4-turbo-2024-04-09
2024-04-15 10:02:41 +08:00
DeanYao
d112dc41b2 Merge pull request #4500 from PeterDaveHello/locale-tw-cht
Improve tw Traditional Chinese locale
2024-04-15 09:47:36 +08:00
SukkaW
2322851ac4 perf: avoid read localStorage on every render 2024-04-14 17:38:54 +08:00
l.tingting
aa084ea09a add timezone in system prompts 2024-04-12 23:07:29 +08:00
l.tingting
6520f9b7eb add knowledge cutoff date for gpt-4-turbo-2024-04-09 2024-04-12 22:44:26 +08:00
butterfly
fd8d0a1746 feat: fix the logtics of client joining webdav url 2024-04-12 14:20:15 +08:00
DeanYao
af3ebacee6 Merge pull request #4507 from ChatGPTNextWeb/chore-fix
feat: fix codes of joining webdav url in client & webdav proxy
2024-04-12 14:07:24 +08:00
butterfly
55d7014301 feat: fix the logtics of client joining webdav url 2024-04-12 14:02:05 +08:00
butterfly
b72d7fbeda feat: fix webdav 逻辑2 2024-04-12 13:46:37 +08:00
butterfly
ee15c14049 feat: fix webdav 逻辑 2024-04-12 13:40:37 +08:00
butterfly
3fc9b91bf1 feat: choe 2024-04-12 10:59:28 +08:00
butterfly
0a8e5d6734 feat: seperate chat page 2024-04-12 10:57:57 +08:00
Peter Dave Hello
1756bdd033 Improve tw Traditional Chinese locale 2024-04-12 00:18:15 +08:00
黑云白土
0cffaf8dc5 Merge branch 'main' into main 2024-04-11 10:30:05 +08:00
DeanYao
55a93e7b47 Merge pull request #4487 from leo4life2/main
Support `gpt-4-turbo` and `gpt-4-turbo-2024-04-09`
2024-04-11 09:26:08 +08:00
黑云白土
5dc5bfb797 Merge branch 'main' into main 2024-04-11 01:24:34 +08:00
Leo Li
f101ee3c4f support new vision models 2024-04-10 05:33:54 -04:00
Leo Li
6319f41b2c add new turbo 2024-04-10 05:18:39 -04:00
Leo Li
6c718ada1b Merge branch 'main' of github.com:ChatGPTNextWeb/ChatGPT-Next-Web 2024-04-10 05:14:44 -04:00
DeanYao
67acc38a1f Merge pull request #4480 from ChatGPTNextWeb/chore-fix
feat: Solve the problem of using openai interface protocol for user-d…
2024-04-10 09:26:21 +08:00
DeanYao
dd1d8509f0 Merge pull request #4476 from dlb-data/dlb-data-patch-1
Update layout.tsx
2024-04-10 09:13:22 +08:00
butterfly
79f342439a feat: Solve the problem of using openai interface protocol for user-defined claude model & add some famous webdav endpoints 2024-04-09 20:49:51 +08:00
DeanYao
13db64f0ec Merge pull request #4479 from ChatGPTNextWeb/chore-fix
feat: white webdav server domain
2024-04-09 18:34:28 +08:00
butterfly
908ce3bbd9 feat: Optimize document 2024-04-09 18:25:51 +08:00
butterfly
df3313971d feat: Optimize code 2024-04-09 18:24:22 +08:00
butterfly
b175132854 feat: Optimize var names 2024-04-09 18:23:52 +08:00
butterfly
4cb0655192 feat: Optimize document 2024-04-09 18:17:00 +08:00
butterfly
8b191bd2f7 feat: white webdav server domain 2024-04-09 18:05:56 +08:00
DeanYao
f3106e3bbb Merge pull request #4477 from ChatGPTNextWeb/chore-fix
feat: 补充文档
2024-04-09 16:50:47 +08:00
butterfly
7fcfbc3729 feat: 补充文档 2024-04-09 16:49:51 +08:00
dlb-data
598468c2b7 Update layout.tsx 2024-04-09 16:34:21 +08:00
dlb-data
84681d3878 Update layout.tsx 2024-04-09 16:24:03 +08:00
DeanYao
c7b14cba4d Merge pull request #4470 from ChatGPTNextWeb/chore-fix
feat: fix system prompt
2024-04-09 10:45:55 +08:00
butterfly
d508127452 feat: fix system prompt 2024-04-09 10:45:09 +08:00
DeanYao
984c79e2d2 Merge pull request #4469 from ChatGPTNextWeb/chore-fix
feat: remove debug code
2024-04-09 09:13:07 +08:00
butterfly
6cb296f952 feat: remove debug code 2024-04-09 09:12:18 +08:00
DeanYao
db533fc166 Merge pull request #4466 from ChatGPTNextWeb/chore-fix
feat: modify some propmt in DEFAULT_INPUT_TEMPLATE about expressing l…
2024-04-08 19:33:27 +08:00
butterfly
02b0e79ba3 feat: modify some propmt in DEFAULT_INPUT_TEMPLATE about expressing latex 2024-04-08 19:27:22 +08:00
DeanYao
1b83dd0a8a Merge pull request #4462 from ChatGPTNextWeb/chore-fix
feat: fix no max_tokens in payload when calling openai vision model
2024-04-08 18:31:52 +08:00
butterfly
9b982b408d feat: fix no max_tokens in payload when calling openai vision model 2024-04-08 18:29:08 +08:00
DeanYao
9b03ab830d Merge pull request #4461 from ChatGPTNextWeb/chore-fix
feat: remove duplicate Input Template
2024-04-08 18:08:48 +08:00
butterfly
264da6798c feat: remove duplicate Input Template 2024-04-08 18:06:17 +08:00
DeanYao
f68b8afa8d Merge pull request #4457 from ChatGPTNextWeb/feat-multi-models
Feat multi models
2024-04-08 17:10:29 +08:00
butterfly
63f9063255 feat: call claude api not in credential 'include' mode 2024-04-08 15:33:27 +08:00
butterfly
6dad353e1c feat: call claude api not in credential 'include' mode 2024-04-08 15:33:02 +08:00
butterfly
5446d8d4a2 feat: fix illegal exports in app/api/anthropic/[...path]/route.ts 2024-04-08 13:59:55 +08:00
butterfly
ef7617d545 feat: configs about app client 2024-04-08 13:41:02 +08:00
butterfly
0fbb560e90 feat: delete returned models in modals function of ClaudeApi instance 2024-04-07 20:05:19 +08:00
butterfly
86b5c55855 feat: roles must alternate between user and assistant in claude, so add a fake assistant message between two user messages 2024-04-07 18:02:31 +08:00
butterfly
768decde93 feat: parse response message 2024-04-07 15:20:27 +08:00
butterfly
3cb4315193 feat: clean codes 2024-04-07 11:50:25 +08:00
butterfly
69b079c86e feat: dev done 2024-04-07 11:32:57 +08:00
DeanYao
9f3fc5eb9f Merge pull request #4417 from xiaotianxt/main
Update apple-touch-icon.png
2024-04-04 08:32:39 +08:00
butterfly
15e595837b feat: settings command dev done 2024-04-02 14:21:49 +08:00
xiaotianxt
17e57bb28e feat: update apple-touch-icon.png 2024-03-30 11:38:20 +08:00
黑云白土
4d0c77b973 更新 constant.ts 2024-03-28 21:42:45 +08:00
黑云白土
f8b180ac44 Update google.ts 2024-03-28 15:52:38 +08:00
黑云白土
cd30368da9 Update constant.ts 2024-03-28 15:51:06 +08:00
黑云白土
27ed57a648 Update utils.ts 2024-03-28 15:49:49 +08:00
DeanYao
e38b527ac2 Merge pull request #3205 from H0llyW00dzZ/summarizelogic
Refactor Summarize Logic
2024-03-28 15:19:32 +08:00
DeanYao
113d9612db Merge pull request #3280 from surkaa/patch-1
Update .env.template 更正单词
2024-03-28 13:40:17 +08:00
DeanYao
6b3daec23f Merge pull request #3314 from H0llyW00dzZ/text-moderation-azure
Feat ChatGPT LLM Api [Console Log] [Text Moderation] [Azure]
2024-03-28 13:38:56 +08:00
DeanYao
e056a1d46d Merge pull request #3405 from Yuliang-Lee/fix/MessageSelectorWarning
fix: MessageSelectorWarning
2024-03-28 11:38:45 +08:00
DeanYao
57026f6262 Merge pull request #3424 from H0llyW00dzZ/serverrside
Refactor Api Common [Server Side] [Console Log]
2024-03-28 11:29:48 +08:00
DeanYao
8ef77f50c3 Merge branch 'main' into serverrside 2024-03-28 11:20:52 +08:00
DeanYao
93e21515e5 Merge pull request #4408 from hmhuming/main
fix docker
2024-03-28 10:28:11 +08:00
hmhuming
24caa3b97b Merge branch 'main' of https://github.com/hmhuming/ChatGPT-Next-Web 2024-03-28 09:18:51 +08:00
DeanYao
c93b36fe79 Merge pull request #3508 from reece00/Mask-language
The language filtering option of the mask is stored
2024-03-27 19:58:30 +08:00
DeanYao
0de9242a26 Merge pull request #3529 from erich2s/chat-item-selected-border
fix(chat-item): selected ChatItem showing border in other pages
2024-03-27 19:00:16 +08:00
hmhuming
53fb52c6c0 fix docker 2024-03-27 17:58:55 +08:00
DeanYao
afaa529ba6 Merge pull request #3870 from Dup4/fix-webdav-check
fix: webdav check httpcode list
2024-03-27 14:04:15 +08:00
DeanYao
43824bd621 Merge pull request #4193 from MrrDrr/env_bug_fix
Update README.md
2024-03-26 17:53:57 +08:00
DeanYao
3c97a4f5a1 Merge pull request #4091 from H0llyW00dzZ/docker-ignore
Update Docker Ignore
2024-03-26 17:44:52 +08:00
DeanYao
711bf190d4 Merge pull request #4264 from ChatGPTNextWeb/dependabot/npm_and_yarn/tauri-apps/cli-1.5.11
chore(deps-dev): bump @tauri-apps/cli from 1.5.7 to 1.5.11
2024-03-26 17:32:01 +08:00
DeanYao
1049006cf9 Merge pull request #4366 from Essmatiko123/dependabot/npm_and_yarn/eslint-plugin-prettier-5.1.3
chore(deps-dev): bump eslint-plugin-prettier from 4.2.1 to 5.1.3
2024-03-26 13:57:50 +08:00
dependabot[bot]
76603d108d chore(deps-dev): bump @tauri-apps/cli from 1.5.7 to 1.5.11
Bumps [@tauri-apps/cli](https://github.com/tauri-apps/tauri) from 1.5.7 to 1.5.11.
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/@tauri-apps/cli-v1.5.7...@tauri-apps/cli-v1.5.11)

---
updated-dependencies:
- dependency-name: "@tauri-apps/cli"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-26 05:42:55 +00:00
DeanYao
5bc3930230 Merge pull request #4389 from ChatGPTNextWeb/dependabot/npm_and_yarn/types/react-18.2.70
chore(deps-dev): bump @types/react from 18.2.14 to 18.2.70
2024-03-26 13:41:13 +08:00
DeanYao
e5edd851b3 Merge pull request #4390 from ChatGPTNextWeb/dependabot/npm_and_yarn/emoji-picker-react-4.9.2
chore(deps): bump emoji-picker-react from 4.5.15 to 4.9.2
2024-03-26 13:35:03 +08:00
dependabot[bot]
dcad400758 chore(deps-dev): bump @types/react from 18.2.14 to 18.2.70
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 18.2.14 to 18.2.70.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-26 05:26:51 +00:00
DeanYao
a1aaea9c55 Merge pull request #4391 from ChatGPTNextWeb/dependabot/npm_and_yarn/types/node-20.11.30
chore(deps-dev): bump @types/node from 20.9.0 to 20.11.30
2024-03-26 13:24:24 +08:00
DeanYao
a4e4286e04 Merge pull request #4186 from MrrDrr/formula_rendering
support \(...\) and \[...\] style math formula
2024-03-25 19:55:57 +08:00
dependabot[bot]
6dd7a6a171 chore(deps-dev): bump @types/node from 20.9.0 to 20.11.30
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 20.9.0 to 20.11.30.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-25 10:47:55 +00:00
dependabot[bot]
8e554a87b0 chore(deps): bump emoji-picker-react from 4.5.15 to 4.9.2
Bumps [emoji-picker-react](https://github.com/ealush/emoji-picker-react) from 4.5.15 to 4.9.2.
- [Release notes](https://github.com/ealush/emoji-picker-react/releases)
- [Commits](https://github.com/ealush/emoji-picker-react/commits)

---
updated-dependencies:
- dependency-name: emoji-picker-react
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-25 10:46:24 +00:00
fred-bf
f1b4c083a4 Merge pull request #4379 from EasonQwQ/main
Fix: Handle empty server response in API call
2024-03-24 14:18:14 +08:00
fred-bf
90af4e3b77 Merge pull request #4381 from ChatGPTNextWeb/fred-bf-patch-3
patch: disable webdav redirect
2024-03-24 14:17:11 +08:00
fred-bf
e8d76a513d patch: disable webdav redirect 2024-03-24 14:15:04 +08:00
kidv
29e03b88c7 Fix: Handle empty server response in API call 2024-03-24 04:07:25 +08:00
fred-bf
ebbd870150 Merge pull request #4353 from H0llyW00dzZ/cherry-pick-webdav
[Cherry Pick] Fix Webdav Syncing Issues
2024-03-21 18:56:26 +08:00
H0llyW00dzZ
c0c54e5709 Fix Webdav Syncing Issues
- [+] feat(route.ts): add endpoint validation and improve error handling
- [+] refactor(route.ts): use targetPath for request validation and error messages
- [+] fix(route.ts): correct targetUrl formation
2024-03-20 01:40:41 +07:00
fred-bf
3ba984d09e Merge pull request #4306 from H0llyW00dzZ/simplify-cherry-pick
[Cherry Pick] Improve [Utils] Check Vision Model
2024-03-19 17:45:57 +08:00
fred-bf
f274683d46 Merge pull request #4322 from imraax/dev
Fix "Enter" bug
2024-03-19 17:44:41 +08:00
fred-bf
e20ce8e335 Merge pull request #4339 from ChatGPTNextWeb/fred-bf-patch-2
feat: update vercel deploy env
2024-03-18 18:25:24 +08:00
fred-bf
9fd750511c feat: update vercel deploy env 2024-03-18 18:24:48 +08:00
Raax
028957fcdc Fix "Enter" bug
Fix Chinese input method "Enter" on Safari
2024-03-16 21:55:16 +08:00
H0llyW00dzZ
a4c54cae60 Improve [Utils] Check Vision Model
- [+] refactor(utils.ts): improve isVisionModel function to use array.some instead of model.includes
2024-03-15 09:38:42 +07:00
fred-bf
cc0eae7153 Merge pull request #4288 from fred-bf/fix/migrate-proxy-url
fix: auto migrate proxy config
2024-03-14 03:05:08 +08:00
Fred
066ca9e552 fix: auto migrate proxy config 2024-03-14 03:03:46 +08:00
fred-bf
7c04a90d77 Merge pull request #4287 from fred-bf/main
feat: bump version
2024-03-14 02:30:58 +08:00
fred-bf
a8a65ac769 Merge branch 'ChatGPTNextWeb:main' into main 2024-03-14 02:30:22 +08:00
Fred
aec3c5d6cc feat: bump version 2024-03-14 02:29:31 +08:00
fred-bf
a22141c2eb Merge pull request #4285 from fred-bf/fix/cors-ssrf
[Bugfix] Fix CORS SSRF security issue
2024-03-14 02:27:55 +08:00
Fred
99aa064319 fix: fix webdav sync issue 2024-03-14 01:58:25 +08:00
Fred
6aaf83f3c2 fix: fix upstash sync issue 2024-03-14 01:56:36 +08:00
Fred
133ce39a13 chore: update cors default path 2024-03-14 01:33:41 +08:00
Fred
8645214654 fix: change matching pattern 2024-03-14 01:26:13 +08:00
Fred
eebc334e02 fix: remove corsFetch 2024-03-14 00:57:54 +08:00
Fred
038fa3b301 fix: add webdav request filter 2024-03-14 00:33:26 +08:00
Fred
9a8497299d fix: adjust upstash api 2024-03-13 23:58:28 +08:00
fred-bf
61ce3868b5 Merge pull request #4279 from SukkaW/package-json-corepack
chore: specify yarn 1 in package.json
2024-03-13 20:09:57 +08:00
SukkaW
844c2a26bc chore: specify yarn 1 in package.json 2024-03-13 13:30:16 +08:00
fred-bf
a15c4d9c20 Merge pull request #4234 from fengzai6/main
Fix EmojiPicker mobile width adaptation and update avatar clicking behavior
2024-03-11 13:59:09 +08:00
fred-bf
ff9f0e60ac Merge pull request #3972 from greenjerry/fix-export-garbled
fix: 修复导出时字符乱码问题
2024-03-07 17:07:16 +08:00
fred-bf
2bf6111bf5 Merge branch 'main' into fix-export-garbled 2024-03-07 17:07:08 +08:00
fengzai6
ad10a11903 Add z-index to avatar 2024-03-07 15:51:58 +08:00
fengzai6
c22153a4eb Revert "fix: No history message attached when for gemini-pro-vision"
This reverts commit c197962851.
2024-03-07 15:46:13 +08:00
fengzai6
5348d57057 Fix EmojiPicker mobile width adaptation and update avatar clicking behavior 2024-03-07 15:36:19 +08:00
fengzai6
052524dabd Merge remote-tracking branch 'upstream/main' 2024-03-07 15:32:09 +08:00
Leo Li
e33d05cfe5 merge 2024-03-05 16:48:10 -05:00
fred-bf
5529ece220 Merge pull request #4218 from ChatGPTNextWeb/fred-bf-patch-1
chore: update GTM_ID definition
2024-03-05 17:37:22 +08:00
fred-bf
e71094d4a8 chore: update GTM_ID definition, close #4217 2024-03-05 17:36:52 +08:00
fred-bf
98aa023d70 Merge pull request #4195 from aliceric27/main
slightly polishes the tw text.
2024-03-04 19:03:23 +08:00
aliceric27
e1066434d0 fix some text 2024-03-03 00:23:00 +08:00
aliceric27
86ae4b2a75 slightly polishes the tw text. 2024-03-02 23:58:23 +08:00
l.tingting
ed8099bf1e Update README.md 2024-03-02 15:26:19 +08:00
l.tingting
524c9beee4 support \(...\) and \[...\] style math formula 2024-03-02 11:08:34 +08:00
fred-bf
99fb9dcf11 Merge pull request #4164 from KSnow616/main
feat: Pasting images into the textbox
2024-02-29 22:14:02 +08:00
fred-bf
1294817103 Merge pull request #4089 from H0llyW00dzZ/cherry-pick
[Cherry Pick] Fix [Utils] Regex trimTopic
2024-02-29 16:31:30 +08:00
Snow Kawashiro
9775660da7 Update chat.tsx 2024-02-28 20:45:42 +08:00
Snow Kawashiro
e7051353eb vision_model_only 2024-02-28 20:38:00 +08:00
Snow Kawashiro
bd19e97cf8 add_image_pasting 2024-02-28 20:05:13 +08:00
fred-bf
8b821ac0c9 Merge pull request #4162 from fred-bf/fix/identify-vision-model
fix: fix the method to detect vision model
2024-02-28 11:35:22 +08:00
Fred
43e5dc2292 fix: fix the method to detect vision model 2024-02-28 11:33:43 +08:00
fred-bf
08fa22749a fix: add max_tokens when using vision model (#4157) 2024-02-27 17:28:01 +08:00
fengzai6
c197962851 fix: No history message attached when for gemini-pro-vision 2024-02-27 15:02:58 +08:00
fred-bf
44a51273be Merge pull request #4149 from fred-bf/feat/auto-detach-scrolling
feat: auto detach scrolling
2024-02-27 11:56:37 +08:00
Fred
e3b3ae97bc chore: clear scroll info 2024-02-27 11:49:44 +08:00
Fred
410a22dc63 feat: auto detach scrolling 2024-02-27 11:43:40 +08:00
Algorithm5838
069766d581 Correct cutoff dates (#4118) 2024-02-27 10:28:54 +08:00
DonaldBear
f22e36e52f feat(tw.ts): added new translations (#4142)
* feat(tw.ts): added new translations

I have translated previously untranslated text in response to the latest update.

* feat(tw.ts): added new translations

I have translated previously untranslated text in response to the latest update.
2024-02-27 00:16:56 +08:00
fred-bf
bc1794fb4a feat: bump version (#4133) 2024-02-26 18:15:00 +08:00
Fred
aacd26c7db feat: bump version 2024-02-26 18:14:10 +08:00
fred-bf
ff166f7b4c chore: adjust for ollama support (#4129) 2024-02-26 17:18:46 +08:00
H0llyW00dzZ
bf1b5c3951 Update Docker Ignore
- [+] chore(dockerignore): update .dockerignore file with more comprehensive ignore rules
2024-02-21 08:46:21 +07:00
H0llyW00dzZ
22baebaf8c [Cherry Pick] Fix [Utils] Regex trimTopic
- [+] fix(utils.ts): update regular expressions in trimTopic function to handle asterisks
2024-02-21 04:19:12 +07:00
H0llyW00dzZ
e756506c18 [Cherry Pick] Improve Github Issue Template (#4041)
* Improve Github Issue Template

- [+] feat(issue templates): update issue templates from markdown to form schema
- [+] feat(issue templates): translate issue templates to Chinese
- [+] remove(issue templates): delete old markdown issue templates

* chore: remove Chinese template issue temporarily

---------

Co-authored-by: Fred <fred@nextchat.dev>
2024-02-20 18:11:02 +08:00
Qiying Wang
fd67f980a5 Fix temperature range (#4083) 2024-02-20 18:05:17 +08:00
TheRam_
e2da3406d2 Add vision support (#4076) 2024-02-20 18:04:32 +08:00
Ikko Eltociear Ashimine
05b6d989b6 chore: fix typo in next.config.mjs (#4072)
verison -> version
2024-02-20 17:59:59 +08:00
H0llyW00dzZ
1d6ee64e1d [Cherry Pick] Fix [UI/UX] [Front End] Settings Page (#4032)
* Fix [UI/UX] [Locales] Correct Spelling

- [+] fix(locales): correct spelling and improve wording in cn.ts and en.ts locale files

* Fix [UI/UX] [Front End] Settings Page

- [+] fix(settings.tsx): correct typo in ApiVerion to ApiVersion
- [+] refactor(settings.tsx): switch Azure.ApiKey to Google.ApiKey in ListItem title and subTitle

* Fix [UI/UX] [Locales] [SK] Correct Typo

- [+] fix(sk.ts): correct typo in ApiVersion key in Slovak locale file
2024-02-12 20:36:52 +08:00
fred-bf
bfefb99192 chore: update tauri dependencies (#4018)
* feat: bump version

* feat: bump version

* chore: update tauri dependencies
2024-02-07 14:12:04 +08:00
Anivie Michaelis
47ae874e4d fix: add support to http scheme. (#3985)
Co-authored-by: fred-bf <157469842+fred-bf@users.noreply.github.com>
2024-02-07 13:48:28 +08:00
fred-bf
d74f636558 Fix/gemini app endpoint (#4017)
* fix: support custom api endpoint

* fix: attach api key to google gemini
2024-02-07 13:46:52 +08:00
fred-bf
b8f0822214 fix: support custom api endpoint (#4016) 2024-02-07 13:40:30 +08:00
fred-bf
0869455612 feat: bump version (#4015)
* feat: bump version

* feat: bump version
2024-02-07 13:38:02 +08:00
fred-bf
bca74241e6 fix: fix gemini issue when using app (#4013)
* chore: update path

* fix: fix google auth logic

* fix: not using header authorization for google api

* chore: revert to allow stream
2024-02-07 13:17:11 +08:00
fred-bf
9d5801fb5f fix: avoiding not operation for custom models (#4010) 2024-02-07 10:31:49 +08:00
H0llyW00dzZ
462a88ae82 Fix [CI/CD] [Vercel] Deploy Preview (#4005)
- [+] feat(.github/workflows/deploy_preview.yml): add 'reopened' event trigger
2024-02-06 17:20:12 +08:00
fred-bf
887bec019a feat: bump version (#4009) 2024-02-06 15:34:43 +08:00
吴迪
d0463b2089 feat(mac): add sign config, fix arm64 build (#4008)
Co-authored-by: vir <wudi@hudiexiaoying.cn>
2024-02-06 15:11:20 +08:00
Shiroki Satsuki
deea4320ad ci: build universal binary for macos (#3711)
* ci: build universal binary for macos

* ci: add tauri args

* ci: restore rust_target

* ci: fallback value for tauri build args

* ci: cache yarn
2024-02-06 15:07:01 +08:00
H0llyW00dzZ
bb26c03141 Feat [UI/UX] [Constants] [Models] gemini-pro KnowledgeCutOffDate (#3997)
- [+] feat(constant.ts): add 'gemini-pro' to KnowledgeCutOffDate constant
2024-02-05 14:44:46 +08:00
H0llyW00dzZ
a5517a1a51 Improve Default System Template (#3996)
* Feat [UI/UX] [Constant] [DEFAULT System Template] replace hardcoded

- [+] feat(constant.ts): replace hardcoded OpenAI with dynamic ServiceProvider variable in DEFAULT_SYSTEM_TEMPLATE

* Improve [UI/UX] [Chat] "fillTemplateWith"

- [+] feat(chat.ts): add DEFAULT_MODELS to modelConfig
- [+] fix(chat.ts): replace replaceAll with regex in output string replacement
- [+] refactor(chat.ts): use const instead of let for cutoff variable
2024-02-05 13:35:51 +08:00
H0llyW00dzZ
4511aa4d21 Fix [UI/UX] [Emoji] Update CDN (#3966)
- [+] fix(emoji.tsx): update CDN url from 'cdn.jsdelivr.net' to 'fastly.jsdelivr.net'
2024-02-03 01:24:37 +08:00
Duplicate4
b25a0545f5 fix: typo (#3871)
Signed-off-by: Dup4 <lyuzhi.pan@gmail.com>
2024-02-03 01:20:10 +08:00
H0llyW00dzZ
1a97bd55c7 Feat [OpenAI] [Models] gpt-3.5-turbo-0125 (#3971)
- [+] feat(constant.ts): add new model 'gpt-3.5-turbo-0125' to DEFAULT_MODELS array
2024-02-03 01:19:25 +08:00
greenjerry
bf711f2ad7 修复导出json和markdown时中文及其他utf8字符乱码问题 2024-02-02 13:58:06 +08:00
Jerry Zhou
e1b065c74a [ImgBot] Optimize images (#3941)
*Total -- 2,603.15kb -> 2,299.83kb (11.65%)

/docs/images/upstash-7.png -- 83.76kb -> 56.04kb (33.1%)
/public/android-chrome-512x512.png -- 95.26kb -> 65.26kb (31.49%)
/docs/images/upstash-1.png -- 23.29kb -> 15.98kb (31.4%)
/docs/images/upstash-6.png -- 66.20kb -> 46.73kb (29.42%)
/docs/images/upstash-5.png -- 66.88kb -> 47.31kb (29.26%)
/docs/images/upstash-4.png -- 37.04kb -> 26.39kb (28.75%)
/docs/images/upstash-3.png -- 49.25kb -> 35.67kb (27.57%)
/public/apple-touch-icon.png -- 18.71kb -> 14.60kb (21.97%)
/public/android-chrome-192x192.png -- 20.52kb -> 16.06kb (21.74%)
/public/macos.png -- 73.77kb -> 57.84kb (21.59%)
/src-tauri/icons/icon.png -- 82.18kb -> 65.26kb (20.58%)
/docs/images/upstash-2.png -- 64.79kb -> 51.85kb (19.97%)
/app/icons/send-white.svg -- 0.95kb -> 0.82kb (13.83%)
/app/icons/close.svg -- 0.96kb -> 0.83kb (13.76%)
/app/icons/reload.svg -- 1.22kb -> 1.06kb (13.36%)
/app/icons/return.svg -- 0.99kb -> 0.86kb (13.33%)
/app/icons/chat.svg -- 1.50kb -> 1.31kb (12.28%)
/app/icons/settings.svg -- 1.10kb -> 0.96kb (12.02%)
/app/icons/eye.svg -- 1.30kb -> 1.17kb (9.92%)
/public/favicon-32x32.png -- 1.70kb -> 1.56kb (8.12%)
/app/icons/eye-off.svg -- 1.84kb -> 1.71kb (6.99%)
/docs/images/head-cover.png -- 1,724.78kb -> 1,608.59kb (6.74%)
/app/icons/bot.svg -- 4.17kb -> 3.89kb (6.57%)
/src-tauri/icons/Square71x71Logo.png -- 6.38kb -> 6.03kb (5.47%)
/src-tauri/icons/Square89x89Logo.png -- 8.61kb -> 8.16kb (5.25%)
/src-tauri/icons/Square44x44Logo.png -- 3.44kb -> 3.30kb (4.09%)
/app/icons/clear.svg -- 1.24kb -> 1.19kb (3.87%)
/app/icons/edit.svg -- 1.61kb -> 1.55kb (3.58%)
/app/icons/pause.svg -- 1.09kb -> 1.05kb (3.58%)
/src-tauri/icons/Square107x107Logo.png -- 11.03kb -> 10.64kb (3.57%)
/app/icons/rename.svg -- 0.89kb -> 0.86kb (3.29%)
/src-tauri/icons/StoreLogo.png -- 4.03kb -> 3.90kb (3.2%)
/app/icons/auto.svg -- 1.30kb -> 1.26kb (3.01%)
/app/icons/chatgpt.svg -- 3.51kb -> 3.42kb (2.64%)
/app/icons/dark.svg -- 0.83kb -> 0.81kb (2.35%)
/app/icons/light.svg -- 2.98kb -> 2.91kb (2.23%)
/app/icons/drag.svg -- 1.36kb -> 1.33kb (2.16%)
/src-tauri/icons/128x128.png -- 13.99kb -> 13.73kb (1.82%)
/app/icons/pin.svg -- 5.83kb -> 5.75kb (1.39%)
/app/icons/break.svg -- 4.53kb -> 4.47kb (1.21%)
/app/icons/connection.svg -- 8.14kb -> 8.05kb (1.01%)
/app/icons/confirm.svg -- 2.24kb -> 2.22kb (1%)
/app/icons/cancel.svg -- 3.54kb -> 3.50kb (0.97%)
/app/icons/robot.svg -- 11.72kb -> 11.62kb (0.89%)
/app/icons/config.svg -- 14.93kb -> 14.81kb (0.78%)
/app/icons/cloud-fail.svg -- 6.25kb -> 6.20kb (0.75%)
/app/icons/cloud-success.svg -- 4.69kb -> 4.66kb (0.75%)
/app/icons/upload.svg -- 10.70kb -> 10.62kb (0.75%)
/app/icons/lightning.svg -- 3.49kb -> 3.47kb (0.62%)
/app/icons/chat-settings.svg -- 8.99kb -> 8.96kb (0.38%)
/src-tauri/icons/Square142x142Logo.png -- 16.15kb -> 16.09kb (0.34%)
/src-tauri/icons/Square150x150Logo.png -- 17.53kb -> 17.51kb (0.07%)

Signed-off-by: ImgBotApp <ImgBotHelp@gmail.com>
Co-authored-by: ImgBotApp <ImgBotHelp@gmail.com>
2024-01-29 14:10:07 +08:00
Takanezawa, Kosuke
eacc911cdc Add Japanese translations for ChatGPT messages (#3936) 2024-01-29 10:38:25 +08:00
H0llyW00dzZ
91a5f4af9a Feat Models [GPT-4-turbo] (#3932)
[+] feat(constant.ts): add new models to DEFAULT_MODELS array
2024-01-27 15:58:35 +08:00
NieXi
f6cdda9029 update google api region (#3934)
Co-authored-by: GH Action - Upstream Sync <action@github.com>
2024-01-27 15:55:44 +08:00
Leo Li
981a3629b6 Add gpt-4-0125-preview (#3924) 2024-01-26 15:00:02 +08:00
Leo Li
3554872d9a Add gpt-4-0125-preview 2024-01-25 15:09:48 -05:00
sungaomeng
118e8f541c fix: cicd, checkout sha Merge pull request #3920 from sungaomeng/main
fix: cicd, checkout sha
2024-01-25 21:25:49 +08:00
孙高猛
3fa55f9022 fix: cicd, checkout sha 2024-01-25 21:17:10 +08:00
fred-bf
1c73c6cc70 Merge pull request #3917 from sungaomeng/main
fix: cicd, remove workflow_dispatch
2024-01-25 20:50:21 +08:00
孙高猛
148c32a383 fix: cicd, remove workflow_dispatch 2024-01-25 20:46:42 +08:00
sungaomeng
ca08062bf3 Merge pull request #3913 from sungaomeng/main
fix: cicd, delete velcel pre
2024-01-25 20:00:11 +08:00
孙高猛
ecbab75a25 fix: cicd, alias domain env name 2024-01-25 19:54:58 +08:00
孙高猛
15d25df245 fix: cicd, delete velcel pre 2024-01-25 19:43:26 +08:00
sungaomeng
b2f7bdca34 Merge branch 'ChatGPTNextWeb:main' into main 2024-01-25 19:38:35 +08:00
sungaomeng
32bcdb8982 fix: cicd, delete vercel pre (#3910) 2024-01-25 19:37:29 +08:00
孙高猛
ba3e7e7974 fix: cicd, vercel alias domain 2024-01-25 19:31:17 +08:00
孙高猛
fae82a30e7 fix: cicd, delete vercel pre 2024-01-25 16:53:03 +08:00
fred-bf
5ce1a55a4f Merge pull request #3909 from sungaomeng/main
fix: ci for vercel deployment
2024-01-25 15:40:11 +08:00
sungaomeng
1132816902 Merge pull request #2 from sungaomeng/sgm/cicd
fix: ci for vercel deployment
2024-01-25 15:28:23 +08:00
sungaomeng
a4c1f1ac47 Merge branch 'main' into sgm/cicd 2024-01-25 15:27:41 +08:00
fred-bf
f619e9df24 chore: change default submit key 2024-01-25 15:22:45 +08:00
孙高猛
43631a3271 fix: cicd, vercel domain suffix secret 2024-01-25 15:17:03 +08:00
fred-bf
04855de495 Merge pull request #3789 from wyxogo/upstream
Fix: Routes `/api/cors/[...path]` was not configured to run with the …
2024-01-25 15:07:40 +08:00
孙高猛
6206ceb49b fix: cicd , add pull_request_target 2024-01-25 15:00:44 +08:00
sungaomeng
628f3ba05f Merge branch 'ChatGPTNextWeb:sgm/cicd' into sgm/cicd 2024-01-25 14:35:32 +08:00
孙高猛
94f534f36f fix vercel alias domains 2024-01-25 14:33:38 +08:00
孙高猛
2f11e7c087 test pr deploy 2024-01-25 11:04:32 +08:00
孙高猛
0739ced407 add workflow_dispatch 2024-01-25 10:57:47 +08:00
孙高猛
8fb34465cd add delete preview scripts 2024-01-25 10:57:47 +08:00
孙高猛
04fe9fadb1 add workflows/deploy preview 2024-01-25 10:57:47 +08:00
孙高猛
998c95da51 test pr deploy 2024-01-24 22:10:02 +08:00
孙高猛
3bc771a0d9 add workflow_dispatch 2024-01-24 22:03:47 +08:00
孙高猛
bdc25adb41 add delete preview scripts 2024-01-24 20:49:30 +08:00
孙高猛
7ec29c2eea add workflows/deploy preview 2024-01-24 20:19:39 +08:00
Fred Liang
b9995e7f70 feat: update dependencies 2024-01-22 15:12:21 +08:00
Fred Liang
31d9d2efcd feat: support setting up GTM 2024-01-22 15:12:21 +08:00
Dup4
86f42d56f2 fix: webdav check httpcode list
Signed-off-by: Dup4 <lyuzhi.pan@gmail.com>
2024-01-18 09:11:13 +08:00
dependabot[bot]
f05bf0a6f6 chore(deps-dev): bump eslint-plugin-prettier from 4.2.1 to 5.1.3
Bumps [eslint-plugin-prettier](https://github.com/prettier/eslint-plugin-prettier) from 4.2.1 to 5.1.3.
- [Release notes](https://github.com/prettier/eslint-plugin-prettier/releases)
- [Changelog](https://github.com/prettier/eslint-plugin-prettier/blob/master/CHANGELOG.md)
- [Commits](https://github.com/prettier/eslint-plugin-prettier/compare/v4.2.1...v5.1.3)

---
updated-dependencies:
- dependency-name: eslint-plugin-prettier
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-15 10:20:50 +00:00
Yasin
a06a2989d2 Merge branch 'ChatGPTNextWeb:main' into upstream 2024-01-11 13:17:56 +08:00
Yifei Zhang
0cf6614186 Merge pull request #3813 from ChatGPTNextWeb/update-cn-qq-info
Update README_CN.md
2024-01-11 01:27:43 +08:00
Yifei Zhang
ddc16cc2be Update README_CN.md 2024-01-11 01:27:31 +08:00
Yifei Zhang
616f0b3e73 Merge pull request #3812 from ChatGPTNextWeb/update-twitter-info
doc: update twitter & QQ group info
2024-01-11 01:26:30 +08:00
Yifei Zhang
bd1056b13f Update README.md 2024-01-11 01:25:20 +08:00
Yasin
5a7b9abe33 Fix: Routes /api/cors/[...path] was not configured to run with the Edge Runtime 2024-01-08 18:43:31 +08:00
Fred Liang
cc86923fd5 fix: import typing error 2024-01-05 21:19:51 +08:00
Fred Liang
916d764477 fix: import language issue 2024-01-05 21:17:26 +08:00
levidius
4a21a8fdae Added Slovak language support
Slovak language was added app/locales/sk.ts and also the language was added to app/locales/index.ts
2024-01-05 21:05:17 +08:00
H0llyW00dzZ
49df5bd590 Chore [Tauri Desktop] Update icons
- [+] chore(icons): update icon files in src-tauri/icons directory
2024-01-05 21:03:44 +08:00
Fred Liang
9eaf492d5b chore: low the google safety setting to avoid unexpected blocking 2023-12-31 19:49:22 +08:00
Fred Liang
a80502f7db fix: fix gemini pro streaming api duplicated issue (#3721)
* fix: streaming duplicated issue

* chore: remove debug logs

* chore: add types defination
2023-12-31 19:08:16 +08:00
Fred Liang
eade013138 fix: update google url description (#3719)
* feat: update new logo & cover image

* fix: update google url description
2023-12-31 18:02:04 +08:00
Fred Liang
912254751a feat: update new logo & cover image (#3716) 2023-12-31 13:24:36 +08:00
Tashvik Srivastava
c3c6f7f7ae update readme (#3715) 2023-12-31 01:43:29 +08:00
Fred Liang
5cf58d9446 feat: support streaming for Gemini Pro (#3688)
* feat: support streaming for Gemini Pro

* feat: display texts smoothly

* chore: remove comments
2023-12-29 03:42:45 +08:00
reece00
3ba598633c Non -GPT model disable system prompt (#3684) 2023-12-28 23:52:45 +08:00
Fred Liang
406530ca69 feat: support vercel speed insight (#3686) 2023-12-28 23:10:19 +08:00
Fred Liang
f8b963df6d chore(docs): update readme for Gemini Pro (#3685)
* chore: update auth value logic

* chore: bump version 2.10.1

* Chore [Package] Downgrade Tauri Builder Version (#3656)

- [+] chore(package.json): update @tauri-apps/cli devDependency to version 1.5.7
- [+] chore(yarn.lock): update @tauri-apps/cli versions to 1.5.7

* chore: update README.md

---------

Co-authored-by: H0llyW00dzZ <priv8@btz.pm>
2023-12-28 22:56:01 +08:00
Fred Liang
d17000975f Release 2.10.1: Support Google Gemini Pro model (#3668)
* chore: update auth value logic

* chore: bump version 2.10.1
2023-12-27 23:45:40 +08:00
H0llyW00dzZ
64a8ba6212 Chore [Package] Downgrade Tauri Builder Version (#3656)
- [+] chore(package.json): update @tauri-apps/cli devDependency to version 1.5.7
- [+] chore(yarn.lock): update @tauri-apps/cli versions to 1.5.7
2023-12-27 23:43:48 +08:00
Yifei Zhang
350ddd2af1 Merge pull request #3215 from akira-cn/main
fix: remove the content-encoding header
2023-12-27 01:26:48 +08:00
Yifei Zhang
c03abdac42 Merge pull request #3570 from Rocky0102/main 2023-12-27 01:24:44 +08:00
ChatGPTNextWeb
bb3cc2c821 Update CN web app link in README.md 2023-12-26 12:49:11 +08:00
ChatGPTNextWeb
0b814eff83 Update Web App link in README.md 2023-12-26 12:47:25 +08:00
H0llyW00dzZ
a91ac91977 Fix [UI/UX] [Emoji] emoji URL (#3647) 2023-12-26 03:44:40 +08:00
Fred Liang
422d70d928 chore: update auth value logic (#3630) 2023-12-25 10:25:43 +08:00
Fred Liang
a4cb859472 Merge pull request #3629 from fredliang44/main
fix: return bearer header when using openai
2023-12-25 10:02:17 +08:00
Fred Liang
19137b79bc fix: return bearer header when using openai 2023-12-25 09:57:40 +08:00
Fred Liang
bd1e311674 Merge pull request #3626 from fredliang44/main
fix: removing `bearer` from auth header when using azure
2023-12-25 05:26:55 +08:00
Fred Liang
c127db335a Merge branch 'ChatGPTNextWeb:main' into main 2023-12-25 05:24:58 +08:00
Fred Liang
f5ed1604aa fix: fix removing bearer header 2023-12-25 05:24:20 +08:00
Fred Liang
771cc9e583 Merge pull request #3625 from fredliang44/main
fix: fix auth key selection when using different model
2023-12-25 05:13:38 +08:00
Fred Liang
7ad1e246a0 Merge branch 'ChatGPTNextWeb:main' into main 2023-12-25 05:12:57 +08:00
Fred Liang
5c638251f8 fix: fix using different model 2023-12-25 05:12:21 +08:00
Fred Liang
0d189dc78e Merge pull request #3622 from fredliang44/main
chore: auto concat messages for gemini-pro
2023-12-25 04:39:03 +08:00
Fred Liang
a9d7253143 Merge branch 'ChatGPTNextWeb:main' into main 2023-12-25 04:38:11 +08:00
Fred Liang
199f29e63c chore: auto concat messages 2023-12-25 04:33:47 +08:00
Fred Liang
c82efceae0 Merge pull request #3620 from fredliang44/main
fix: fix issue cannot using gemini-pro with code auth
2023-12-25 04:09:53 +08:00
Fred Liang
cad461b121 chore: remove console log 2023-12-25 04:09:38 +08:00
Fred Liang
5af68ac545 fix: fix issue https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/3616 2023-12-25 04:07:35 +08:00
Fred Liang
c0ce70ccc7 Merge pull request #3617 from H0llyW00dzZ/fixfixbug
Fix [Bug] Issue ChatGPTNextWeb#3615
2023-12-25 03:47:16 +08:00
Fred Liang
753c518d33 chore: update how to identify google model 2023-12-25 03:46:35 +08:00
Fred Liang
b9ca6694f7 Merge pull request #3619 from H0llyW00dzZ/fix_llm_issue 2023-12-25 03:32:02 +08:00
H0llyW00dzZ
0c116251b1 Revert "Fix [TypesScript] [LLM Api] Chaining Model"
This reverts commit 8ca525dc7a.

Reason: It's suddenly stopped lmao
2023-12-25 01:45:24 +07:00
H0llyW00dzZ
e9def2cdc5 Refactor [Model] [LLM Api] Chaining Model
- [+] refactor(model.ts): change forEach loop to arrow function for readability and consistency
- [+] fix(model.ts): mark 'provider' property as optional in modelTable type
- [+] fix(model.ts): use optional chaining when assigning provider property in modelTable
2023-12-25 01:12:44 +07:00
H0llyW00dzZ
8ca525dc7a Fix [TypesScript] [LLM Api] Chaining Model
- [+] fix(api.ts): make provider property optional in LLMModel interface
2023-12-25 01:12:44 +07:00
H0llyW00dzZ
281fe6927a Feat [Server Side] Google Api Configuration
- [+] feat(server.ts): add support for Google API configuration variables
2023-12-25 00:37:49 +07:00
H0llyW00dzZ
35471a41c8 Fix & Feat [Auth] Api Key Variable
- [+] fix(auth.ts): fix variable name from serverApiKey to systemApiKey
- [+] feat(auth.ts): add support for Google API key in addition to Azure API key
2023-12-25 00:37:48 +07:00
ChatGPTNextWeb
bda3098a31 Merge pull request #3609 from fredliang44/main
[Feature] support Gemini Pro API
2023-12-24 18:42:41 +08:00
Fred Liang
1e05eb1d60 chore: update docs for gemini pro 2023-12-24 17:54:33 +08:00
Fred Liang
6369b902bf chore: update README for google gemini pro model 2023-12-24 17:43:41 +08:00
Fred Liang
aea794e522 chore: remove openai log from google api handler 2023-12-24 17:37:07 +08:00
Fred Liang
7c3dfb7bae feat: support custom gemini pro params 2023-12-24 17:24:04 +08:00
Fred Liang
75057f9a91 chroe: support error reason for gemini pro 2023-12-24 04:32:25 +08:00
Fred Liang
7026bd926a chroe: update prompts 2023-12-24 04:24:38 +08:00
Fred Liang
ae19a0dc5f chroe: update model name 2023-12-24 04:22:12 +08:00
Fred Liang
7d9a2132cb fix: fix server token fetch policy 2023-12-24 03:57:30 +08:00
Fred Liang
20f2f61349 feat: support set api key from client side 2023-12-24 03:44:13 +08:00
Fred Liang
4169431f2c fix: fix add api auth 2023-12-24 03:05:23 +08:00
Fred Liang
45798f993d fix: fix type errors 2023-12-24 02:39:13 +08:00
Fred Liang
ae0d68c27e fix: fix llm models field 2023-12-24 02:39:13 +08:00
Fred Liang
070b191ffe Merge branch 'ChatGPTNextWeb:main' into main 2023-12-24 02:16:51 +08:00
Fred Liang
778e88cb56 chore: resolve conflict 2023-12-24 02:15:30 +08:00
Yifei Zhang
d1fcfa05db Merge pull request #3574 from RoboticsAndCloud/main
Update README.md
2023-12-23 23:56:35 +08:00
ChatGPTNextWeb
f4754dc5fc Merge pull request #3588 from qiuqfang/main
chore: update readme
2023-12-23 23:31:23 +08:00
ChatGPTNextWeb
0f885e73c3 Merge pull request #3607 from fredliang44/main
chore: update product name from`ChatGPT-Next-Web` to `NextChat` for legal concerns of the ChatGPT trademark. The repo name will not be changed for now.
2023-12-23 23:29:18 +08:00
Fred Liang
75acd4c1aa chore: append previous name 2023-12-23 22:21:27 +08:00
Fred Liang
3ef0621eb0 refactor: update product name 2023-12-23 16:48:16 +08:00
GH Action - Upstream Sync
b1db9ead0d Merge branch 'main' of https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web 2023-12-23 00:41:51 +00:00
Yifei Zhang
9cc6000d89 Merge pull request #3601 from zeabur/main
docs: add deploy to Zeabur button in README
2023-12-22 14:59:36 +08:00
Yuhang
42515899d9 docs: Add deploy to Zeabur button in REAME for Chinese 2023-12-22 14:58:30 +08:00
Yuhang
1d096eeb17 docs: Add deploy to Zeabur button in README 2023-12-22 14:57:54 +08:00
pdx
bc5b8f0ff9 Update README.md 2023-12-20 13:55:53 +08:00
pdx
bf412fd309 Update README.md 2023-12-20 13:55:22 +08:00
Fei
6e1f4246b8 Merge branch 'ChatGPTNextWeb:main' into main 2023-12-19 11:27:38 -08:00
Yifei Zhang
ed88619623 Merge pull request #3565 from ChatGPTNextWeb/dependabot/npm_and_yarn/hello-pangea/dnd-16.5.0
chore(deps): bump @hello-pangea/dnd from 16.3.0 to 16.5.0
2023-12-19 15:33:28 +08:00
Yifei Zhang
37b8922348 Merge pull request #3566 from ChatGPTNextWeb/dependabot/npm_and_yarn/tauri-apps/cli-1.5.8
chore(deps-dev): bump @tauri-apps/cli from 1.4.0 to 1.5.8
2023-12-19 15:33:19 +08:00
Yifei Zhang
4a23a0064b Merge pull request #3567 from ChatGPTNextWeb/dependabot/npm_and_yarn/fuse.js-7.0.0
chore(deps): bump fuse.js from 6.6.2 to 7.0.0
2023-12-19 15:33:10 +08:00
Yifei Zhang
e012b1e5d1 Merge pull request #3061 from ChatGPTNextWeb/dependabot/npm_and_yarn/babel/traverse-7.23.2
chore(deps): bump @babel/traverse from 7.21.3 to 7.23.2
2023-12-19 15:32:55 +08:00
Fei
2bc84ec908 Update README.md 2023-12-18 22:43:59 -06:00
Rocky
8efe26ed57 Merge pull request #1 from Rocky0102/Rocky0102-patch-proper-accept-header
Update api.ts to set proper header "Accept"
2023-12-18 20:36:59 +08:00
Rocky
24d7dc9bf9 Update api.ts to set proper header "Accept"
So it can work fine for some GPT forward service.
2023-12-18 20:34:37 +08:00
dependabot[bot]
4b7139d9ae chore(deps): bump fuse.js from 6.6.2 to 7.0.0
Bumps [fuse.js](https://github.com/krisk/Fuse) from 6.6.2 to 7.0.0.
- [Release notes](https://github.com/krisk/Fuse/releases)
- [Changelog](https://github.com/krisk/Fuse/blob/main/CHANGELOG.md)
- [Commits](https://github.com/krisk/Fuse/compare/v6.6.2...v7.0.0)

---
updated-dependencies:
- dependency-name: fuse.js
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-18 10:30:29 +00:00
dependabot[bot]
1f35624121 chore(deps-dev): bump @tauri-apps/cli from 1.4.0 to 1.5.8
Bumps [@tauri-apps/cli](https://github.com/tauri-apps/tauri) from 1.4.0 to 1.5.8.
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/@tauri-apps/cli-v1.4.0...@tauri-apps/cli-v1.5.8)

---
updated-dependencies:
- dependency-name: "@tauri-apps/cli"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-18 10:30:04 +00:00
dependabot[bot]
860c5b8f20 chore(deps): bump @hello-pangea/dnd from 16.3.0 to 16.5.0
Bumps [@hello-pangea/dnd](https://github.com/hello-pangea/dnd) from 16.3.0 to 16.5.0.
- [Release notes](https://github.com/hello-pangea/dnd/releases)
- [Changelog](https://github.com/hello-pangea/dnd/blob/main/CHANGELOG.md)
- [Commits](https://github.com/hello-pangea/dnd/compare/v16.3.0...v16.5.0)

---
updated-dependencies:
- dependency-name: "@hello-pangea/dnd"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-18 10:29:13 +00:00
Yifei Zhang
a454c39ca0 Merge pull request #3511 from KevenLi8888/add-dockerignore
ci: add .dockerignore to exclude env files to prevent potential key leakages 避免潜在的密钥泄漏
2023-12-18 15:26:32 +08:00
Yifei Zhang
6938910f08 Merge pull request #3559 from RoboticsAndCloud/patch-1
Update README.md
2023-12-18 15:25:49 +08:00
Fei
2c63dde6c5 Update README.md 2023-12-17 21:24:36 -06:00
Yifei Zhang
e69d20a209 Merge pull request #3538 from MCheping8108/main
fix sync.yml
2023-12-18 01:16:54 +08:00
和平peaceful
0b731edd21 fix sync.yml 2023-12-15 21:24:37 +08:00
Yifei Zhang
efdd61595e Merge pull request #3535 from ChatGPTNextWeb/Yidadaa-patch-1
Update README_CN.md
2023-12-15 17:47:47 +08:00
Yifei Zhang
d676f88fed Update README_CN.md 2023-12-15 17:47:36 +08:00
Yifei Zhang
07f2cf7ad0 Merge pull request #3532 from ChatGPTNextWebTeam/update-emoji-url
fix: update emoji url
2023-12-15 15:20:15 +08:00
Yifei Zhang
bcb520ed3b Update emoji.tsx 2023-12-15 15:18:02 +08:00
Eric Huang
943a2707d2 fix(chat-item): selected chat-item showing border in other pages 2023-12-15 09:37:37 +08:00
Qian (Keven) Li
93cee18300 ci: add .dockerignore to exclude env files 2023-12-12 02:26:49 -05:00
reece00
1442337e3c The language filtering option of the mask is stored 2023-12-12 02:22:22 +08:00
Yifei Zhang
cae4655785 Update tauri.conf.json 2023-12-11 15:59:37 +08:00
Yifei Zhang
28c12606a4 Merge pull request #3455 from H0llyW00dzZ/trimtopic 2023-12-06 18:58:38 +08:00
H0llyW00dzZ
54df355014 Fix Trim Topic in Indonesia Language
- [+] fix(utils.ts): fix trimTopic function to remove double quotes from the start and end of the string, and remove specified punctuation from the end of the string
2023-12-05 07:26:11 +07:00
H0llyW00dzZ
8dc8682078 Fix Api Common [Server Side]
- [+] fix(common.ts): improve handling of OpenAI-Organization header
 - Check if serverConfig.openaiOrgId is defined and not an empty string
 - Log the value of openaiOrganizationHeader if present, otherwise log that the header is not present
 - Conditionally delete the OpenAI-Organization header from the response if [Org ID] is undefined or empty (not setup in ENV)
2023-12-04 13:33:23 +07:00
H0llyW00dzZ
36e9c6ac4d Refactor Api Common [Server Side] [Console Log]
- [+] refactor(common.ts): remove unnecessary console.log for [Org ID] in requestOpenai function
- [+] refactor(common.ts): conditionally delete OpenAI-Organization header from response if [Org ID] is not set up in ENV
2023-12-01 19:49:12 +07:00
frankylli
10ea9bf1e3 fix: MessageSelectorWarning 2023-11-29 16:25:15 +08:00
Yifei Zhang
cf50299b14 Update README_CN.md 2023-11-29 11:39:21 +08:00
Yifei Zhang
2c12be62c4 Merge pull request #3383 from H0llyW00dzZ/auth 2023-11-27 15:51:27 +08:00
Yifei Zhang
4636a75b5e Merge pull request #3382 from H0llyW00dzZ/docs 2023-11-27 15:47:39 +08:00
Yifei Zhang
03756e364a Merge pull request #3372 from H0llyW00dzZ/fixserverside 2023-11-27 15:47:24 +08:00
Yifei Zhang
ce1715c79e Merge pull request #3369 from H0llyW00dzZ/chatz 2023-11-27 15:47:01 +08:00
Yifei Zhang
a62ab3c649 Merge pull request #3354 from AnYiEE/fix-type 2023-11-27 15:45:42 +08:00
Yifei Zhang
bfb7b988f4 Merge pull request #3356 from durianice/pr_merge 2023-11-27 15:44:03 +08:00
Yifei Zhang
84f41262f5 Update README.md 2023-11-27 15:28:34 +08:00
H0llyW00dzZ
dda40e29f4 Fix & Refactor UI/UX Page [Settings]
- [+] fix(settings.tsx): fix conditional rendering of ListItem based on clientConfig.isApp
- [+] refactor(settings.tsx): improve readability of conditional rendering code
2023-11-27 08:07:53 +07:00
H0llyW00dzZ
7df868e22a Revert "Fix & Refactor UI/UX Page [Auth]"
This reverts commit d76e744eab.

Reason: Move to commits bf5e7aaa48
2023-11-27 07:12:57 +07:00
H0llyW00dzZ
bf5e7aaa48 Feat UI/UX Page [Settings]
- [+] feat(settings.tsx): set useCustomConfig to true if clientConfig.isApp is truthy
2023-11-27 07:03:41 +07:00
H0llyW00dzZ
d76e744eab Fix & Refactor UI/UX Page [Auth]
- [+] fix(auth.tsx): fix condition to set custom endpoint to true if it's app
- [+] refactor(auth.tsx): update accessStore to set useCustomConfig to true
2023-11-27 05:44:26 +07:00
H0llyW00dzZ
6f5699fe09 Chore Docs (README.md)
- [+] chore(README.md): add support for Korean and Indonesian languages
2023-11-27 04:49:15 +07:00
H0llyW00dzZ
f9d916925e Fixup Api Common [Server Side] [Console Log]
- [+] fix(common.ts): fix condition to check if serverConfig.openaiOrgId is not undefined
2023-11-25 10:23:16 +07:00
H0llyW00dzZ
f9258878db Fix Api Common [Server Side] [Console Log]
- [+] fix(common.ts): fix console.log statement for [Org ID] to handle undefined value
2023-11-25 10:10:06 +07:00
H0llyW00dzZ
ef9e86b50d Fix UI/UX Page Chats [Memory Prompt] [Stored Local Storage]
- [+] fix(chat.ts): update the memory prompt in onFinish callback
- [+] feat(chat.ts): update the current session with lastSummarizeIndex and memoryPrompt
2023-11-25 05:03:34 +07:00
Yifei Zhang
b21931c667 Update README.md 2023-11-24 11:36:14 +08:00
Yifei Zhang
06de3f5e69 Update README.md 2023-11-24 10:33:33 +08:00
durian
261a8fd83b fix(CUSTOM_MODELS):#3349 Dependency chatStore otherwise the session and view will not updated 2023-11-23 15:37:57 +08:00
安忆
6527074cde fix(locales): type error in pt.ts 2023-11-23 11:02:20 +08:00
H0llyW00dzZ
fe0f078353 Feat ChatGPT LLM Api [Console Log] [Text Moderation] [Azure]
[+] fix(openai.ts): fix parsing error in ChatGPTApi's message handler
[+] feat(openai.ts): add logging for flagged categories in text moderation
2023-11-19 19:49:52 +07:00
Yidadaa
f2485931d9 feat: better animation speed 2023-11-19 19:15:11 +08:00
Yifei Zhang
4f8a0b7711 Merge pull request #3308 from yuri-filipe/main 2023-11-19 18:45:31 +08:00
Yifei Zhang
2dde55050e Merge pull request #3311 from Yidadaa/bugfix-1119 2023-11-19 18:44:14 +08:00
Yidadaa
6aade62ce2 feat: close #3300 support multiple api keys 2023-11-19 18:42:30 +08:00
Yidadaa
45b88ebb2a feat: close #3304 use = instead of : to map model name in CUSTOM_MODELS 2023-11-19 18:34:39 +08:00
Yidadaa
dc7159a450 feat: close #3301 enable or diable default models with -all / +all 2023-11-19 18:32:54 +08:00
Yidadaa
536ace8e10 feat: animate streaming response to make more smooth 2023-11-19 18:24:51 +08:00
Yifei Zhang
16b2a3e66e Update vercel.json 2023-11-19 04:23:25 +08:00
Yuri Filipe
6f135a0cce Translation of the application into Brazilian Portuguese 2023-11-18 15:12:15 -03:00
Yifei Zhang
cf220dd2eb Update vercel.json 2023-11-17 15:22:11 +08:00
Yifei Zhang
914f4fb862 Update vercel.json 2023-11-17 15:11:03 +08:00
Yifei Zhang
7bdb68eecf config: disable ip forward in vercel json config 2023-11-17 15:10:51 +08:00
Yifei Zhang
3c510cfaf0 chore: update gh actions nodejs version to 18 2023-11-17 11:56:42 +08:00
Yifei Zhang
401fa198c9 Update tauri.conf.json 2023-11-17 11:29:52 +08:00
Yifei Zhang
600df4f63a Merge pull request #3290 from Eric-2369/main 2023-11-17 11:26:21 +08:00
Eric-2369
74eb42c111 config: exclude hkg1 from the deployment regions of the edge function 2023-11-16 11:33:10 +08:00
SurKaa
39f3afd52c Update .env.template 更正单词 2023-11-16 09:22:56 +08:00
Yidadaa
9876a1aeca fix: #3275 refuse on server side if hide user api key 2023-11-16 00:53:11 +08:00
Yifei Zhang
d898ffce23 Merge pull request #3261 from H0llyW00dzZ/clients 2023-11-14 16:07:16 +08:00
H0llyW00dzZ
f1772f4625 Fix UI/UX Page [Settings]
[+] fix(settings.tsx): fix condition to show balance query in settings component
[+] fix(settings.tsx): add condition to hide balance query for app clients
2023-11-14 15:04:57 +07:00
Yifei Zhang
9da455a7ea Merge pull request #3248 from Yidadaa/dependabot/npm_and_yarn/nanoid-5.0.3 2023-11-14 01:24:10 +08:00
Yifei Zhang
2bd6342309 Merge pull request #3249 from Yidadaa/dependabot/npm_and_yarn/emoji-picker-react-4.5.15 2023-11-14 01:23:50 +08:00
Yifei Zhang
5e73577088 Merge pull request #3250 from Yidadaa/dependabot/npm_and_yarn/types/node-20.9.0 2023-11-14 01:23:37 +08:00
Yifei Zhang
5156a80738 Merge pull request #3251 from Yidadaa/dependabot/npm_and_yarn/types/spark-md5-3.0.4 2023-11-14 01:23:17 +08:00
dependabot[bot]
a9d605ed30 chore(deps): bump nanoid from 4.0.2 to 5.0.3
Bumps [nanoid](https://github.com/ai/nanoid) from 4.0.2 to 5.0.3.
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/4.0.2...5.0.3)

---
updated-dependencies:
- dependency-name: nanoid
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-13 17:22:51 +00:00
dependabot[bot]
7d1fae32cd chore(deps): bump emoji-picker-react from 4.5.1 to 4.5.15
Bumps [emoji-picker-react](https://github.com/ealush/emoji-picker-react) from 4.5.1 to 4.5.15.
- [Release notes](https://github.com/ealush/emoji-picker-react/releases)
- [Commits](https://github.com/ealush/emoji-picker-react/commits)

---
updated-dependencies:
- dependency-name: emoji-picker-react
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-13 17:22:50 +00:00
Yifei Zhang
9f17b45bc4 Merge pull request #3252 from Yidadaa/dependabot/npm_and_yarn/mermaid-10.6.1 2023-11-14 01:21:11 +08:00
dependabot[bot]
a64c9dae42 chore(deps): bump mermaid from 10.3.1 to 10.6.1
Bumps [mermaid](https://github.com/mermaid-js/mermaid) from 10.3.1 to 10.6.1.
- [Release notes](https://github.com/mermaid-js/mermaid/releases)
- [Changelog](https://github.com/mermaid-js/mermaid/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/mermaid-js/mermaid/compare/v10.3.1...v10.6.1)

---
updated-dependencies:
- dependency-name: mermaid
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-13 10:45:59 +00:00
dependabot[bot]
5fbf4c394c chore(deps-dev): bump @types/spark-md5 from 3.0.2 to 3.0.4
Bumps [@types/spark-md5](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/spark-md5) from 3.0.2 to 3.0.4.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/spark-md5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-13 10:45:47 +00:00
dependabot[bot]
1e5153173c chore(deps-dev): bump @types/node from 20.3.3 to 20.9.0
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 20.3.3 to 20.9.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-13 10:45:37 +00:00
Yifei Zhang
011b52d07d Update README.md 2023-11-13 16:53:36 +08:00
Yifei Zhang
d033168d80 fix: #3241 should not ensure openai url non-empty 2023-11-13 10:53:30 +08:00
Yifei Zhang
fdca9e59de Merge pull request #3233 from nanaya-tachibana/nanaya/model-selection 2023-11-13 10:44:03 +08:00
Yifei Zhang
549a2fd206 Merge pull request #3237 from Yidadaa/latex 2023-11-12 19:47:26 +08:00
Yidadaa
a0cd939bfd fix: #2841 dollar sign conflict with latex math 2023-11-12 19:45:58 +08:00
Yifei Zhang
4f52679ec6 Merge pull request #3236 from Yidadaa/latex 2023-11-12 19:35:22 +08:00
Yidadaa
b52e237044 fix: #3189 should correct math eq in exporter 2023-11-12 19:33:19 +08:00
Yifei Zhang
17b329bb63 Merge pull request #3232 from H0llyW00dzZ/language 2023-11-12 18:00:02 +08:00
nanaya
3a654ba199 UI (model selection): hide unavailable model options 2023-11-12 11:18:14 +08:00
H0llyW00dzZ
5ba3fc9321 Revert "Fix UI/UX Page Local Language [Exporter Message]"
This reverts commit a46f08154e.
Reason : better after instead of before
2023-11-12 01:56:31 +07:00
H0llyW00dzZ
a46f08154e Fix UI/UX Page Local Language [Exporter Message]
fix(locales): fix incorrect description title in en.ts and id.ts
 - Change "Only messages after clearing the context will be displayed" to "Only messages before clearing the context will be displayed" in en.ts
 - Change "Hanya pesan setelah menghapus konteks yang akan ditampilkan" to "Hanya pesan sebelum menghapus konteks yang akan ditampilkan" in id.ts
2023-11-12 01:50:35 +07:00
H0llyW00dzZ
0f6ed9c293 Feat UI/UX Page Local Language [Exporter Message]
[+] fix(exporter.tsx): update the text in the ExportMessageModal component to use the localized title from the locale file
[+] feat(cn.ts, en.ts, id.ts): add localized title for the Exporter Description in the respective locale files
2023-11-12 00:53:15 +07:00
Yifei Zhang
295864d36b Merge pull request #3231 from Yidadaa/bugfix-1112 2023-11-12 01:23:56 +08:00
Yidadaa
be6d45e49f feat: close #3222 share message list should start from clear context index 2023-11-12 01:21:39 +08:00
Yifei Zhang
22b6987249 Merge pull request #3230 from Yidadaa/bugfix-1112 2023-11-12 00:52:21 +08:00
Yidadaa
64647b0bb3 chore: update doc for mapped CUSTOM_MODELS 2023-11-12 00:49:58 +08:00
Yidadaa
a5a1f2e8ad feat: CUSTOM_MODELS support mapper 2023-11-12 00:46:21 +08:00
Yifei Zhang
8bd39f33e0 Merge pull request #3229 from Yidadaa/bugfix-1112 2023-11-12 00:31:15 +08:00
Yidadaa
be9774943b feat: #3224 auto switch to first avaliable model 2023-11-12 00:29:36 +08:00
akira-cn
dd6e79922a fix: remove the content-encoding header 2023-11-10 20:42:12 +08:00
akira-cn
bf84269520 fix: remove the content-encoding header 2023-11-10 20:38:53 +08:00
Yifei Zhang
943214c6a7 Update README_CN.md 2023-11-10 18:21:22 +08:00
Yifei Zhang
ca792669fc Update constant.ts 2023-11-10 18:20:51 +08:00
Yifei Zhang
78d68a9949 Merge pull request #3214 from Ensteinjun/main 2023-11-10 15:58:28 +08:00
Ensteinjun
6b2db97347 fixbug 2023-11-10 15:44:07 +08:00
Yifei Zhang
2bfb362832 Update tauri.conf.json 2023-11-10 15:19:31 +08:00
Yidadaa
cb140e482f fix: #3207 ensure corner case 2023-11-10 15:15:43 +08:00
Yifei Zhang
e6b72ac1ff Update README.md 2023-11-10 02:59:47 +08:00
Yifei Zhang
8032e6d68d Update README.md 2023-11-10 02:59:30 +08:00
Yidadaa
c7e0a6f37f doc: update azure env vars 2023-11-10 02:57:46 +08:00
Yifei Zhang
1141cd2e6e Merge pull request #3206 from Yidadaa/azure 2023-11-10 02:55:12 +08:00
Yidadaa
c9dd953817 fixup 2023-11-10 02:50:50 +08:00
Yidadaa
b7ffca031e feat: close #935 add azure support 2023-11-10 02:43:37 +08:00
H0llyW00dzZ
544bab0fe2 Refactor Summarize Logic
[+] chore(chat.ts): remove unnecessary comment and refactor variable name
[+] feat(chat.ts): add stream: false to config object
2023-11-09 20:56:45 +07:00
Yifei Zhang
fd2f441e02 feat: wont send max_tokens 2023-11-09 20:45:25 +08:00
Yifei Zhang
3b3ebda34b fix: #3196 3.5-turbo-1106 should use old cutoff date 2023-11-09 11:01:41 +08:00
Yifei Zhang
87e3d663a2 fix: #3192 use smaller max_tokens as default 2023-11-09 11:00:09 +08:00
Yidadaa
d0a1d910d4 fix: #3186 enable max_tokens in chat payload 2023-11-09 03:19:13 +08:00
Yifei Zhang
33b97082fa Merge pull request #3192 from Yidadaa/bugfix-1109 2023-11-09 03:05:41 +08:00
Yidadaa
d93f05f511 feat: close #3187 use CUSTOM_MODELS to control model list 2023-11-09 03:01:29 +08:00
Yifei Zhang
3dc29197e1 Merge pull request #3191 from Yidadaa/bugfix-1109 2023-11-09 02:23:20 +08:00
Yidadaa
fbc0236748 fix: #3174 should prompt to confirm to delete chat 2023-11-09 02:03:05 +08:00
Yifei Zhang
1304a3943a Merge pull request #3190 from Yidadaa/bugfix-1109 2023-11-09 01:54:09 +08:00
Yidadaa
8c0ba1aee2 feat: close #2954 chat summary should be copyable 2023-11-09 01:51:33 +08:00
Yifei Zhang
47154773f2 Merge pull request #3181 from AnYiEE/docs-typo 2023-11-08 18:37:59 +08:00
Yifei Zhang
deca9bf06d Merge pull request #3183 from hekbobo/patch-1 2023-11-08 18:37:35 +08:00
bob
6ab4c9be2e Update cloudflare-pages-cn.md
cloudflare 构建命令
2023-11-08 17:40:28 +08:00
安忆
3a519612a0 docs: fix typo in #3171 2023-11-08 15:53:45 +08:00
Yifei Zhang
d1ec26ae83 Merge pull request #3171 from AnYiEE/docs-update 2023-11-08 15:31:17 +08:00
安忆
9cb889c34f docs: up to date 2023-11-08 07:09:52 +08:00
Yifei Zhang
77f27b7948 Merge pull request #3166 from Yidadaa/bugfix-1107 2023-11-08 01:08:00 +08:00
Yifei Zhang
dbee5bc515 Update tauri.conf.json 2023-11-08 01:05:07 +08:00
Yidadaa
54a5332834 feat: sort model by name 2023-11-08 01:04:20 +08:00
Yifei Zhang
8af164c99f Merge pull request #3165 from Yidadaa/bugfix-1107 2023-11-08 00:36:09 +08:00
Yidadaa
c7321fddfb fixup 2023-11-08 00:34:31 +08:00
Yidadaa
c5ca278253 feat: close #2908 allow to disable parse settings from link 2023-11-08 00:30:02 +08:00
Yidadaa
638fdd8c3e feat: default disable balance query 2023-11-08 00:20:34 +08:00
Yifei Zhang
3407b57a51 Merge pull request #3163 from Yidadaa/bugfix-1107 2023-11-07 23:56:29 +08:00
Yidadaa
836bf836d3 fix: #3152 system prompt should be injected 2023-11-07 23:54:30 +08:00
Yidadaa
6e52d14180 fix: #3016 disable sidebar transition on ios 2023-11-07 23:30:09 +08:00
Yidadaa
cdf0311d27 feat: add claude and bard 2023-11-07 23:22:11 +08:00
Yifei Zhang
1096fe55e1 Merge pull request #3143 from H0llyW00dzZ/newmodels 2023-11-07 10:58:40 +08:00
H0llyW00dzZ
d2d615c84a Refactor KnowledgeCutoff
[+] fix(constant.ts): update DEFAULT_SYSTEM_TEMPLATE to include knowledgeCutoff and time variables
[+] feat(chat.ts): add support for injecting system prompts based on model configuration
2023-11-07 06:42:55 +07:00
H0llyW00dzZ
9f26c8cecb Fix Typo [GPT-4]
[+] fix(constant.ts): fix typo in model name, change "ggpt-4-1106-preview" to "gpt-4-1106-preview"
2023-11-07 06:33:07 +07:00
H0llyW00dzZ
cd1f082c52 Feat Models [GPT-4] [GPT-3.5] [128K context]
[+] feat(constant.ts): add new models to DEFAULT_MODELS array
2023-11-07 06:08:06 +07:00
Yidadaa
5610f423d0 feat: add multi-model support 2023-10-30 02:07:11 +08:00
Yifei Zhang
b90dfb48ee Merge pull request #3095 from Yidadaa/revert-3013-main 2023-10-26 18:34:44 +08:00
Yifei Zhang
70e67a686b Revert "fix serviceWorker cache: auto caching files by workbox" 2023-10-26 00:06:09 +08:00
Yifei Zhang
d1c3d900ab Update README_KO.md 2023-10-23 14:38:26 +08:00
Yifei Zhang
e8a4ad1001 Update README_KO.md 2023-10-23 14:38:04 +08:00
Yifei Zhang
ccac85b079 Update README_CN.md 2023-10-23 14:37:48 +08:00
Yifei Zhang
f92fefb6cd Update synchronise-chat-logs-cn.md 2023-10-23 14:36:42 +08:00
dependabot[bot]
b799213a14 chore(deps): bump @babel/traverse from 7.21.3 to 7.23.2
Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.21.3 to 7.23.2.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.23.2/packages/babel-traverse)

---
updated-dependencies:
- dependency-name: "@babel/traverse"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-18 23:52:00 +00:00
Yifei Zhang
cc565cf57e Merge pull request #3013 from jessegpt/main 2023-10-16 12:02:26 +08:00
Yifei Zhang
4a56e9c884 Merge pull request #3019 from eltociear/patch-2 2023-10-16 12:02:04 +08:00
Yifei Zhang
d11d906fa2 Merge pull request #3033 from Yidadaa/Yidadaa-patch-1 2023-10-16 11:58:36 +08:00
Yifei Zhang
65c4a0c319 feat: close #3031 user can set larger font size 2023-10-16 11:52:45 +08:00
Yifei Zhang
55bcf78efe Update README.md 2023-10-15 20:08:08 +08:00
Ikko Eltociear Ashimine
f5a2ce52aa Fix typo in README.md
passsword -> password
2023-10-14 15:22:41 +09:00
Jesse
0dcbfd746e fix serviceWorker cache 2023-10-13 15:41:00 +04:00
Yifei Zhang
06e043d3f1 Merge pull request #3003 from suravshrestha/patch1 2023-10-13 00:41:16 +08:00
Surav Shrestha
986d34fb3e docs: fix typo in app/masks/en.ts 2023-10-12 21:22:35 +05:45
Yifei Zhang
5296ab1b09 Merge pull request #2996 from MCheping8108/main 2023-10-12 18:22:52 +08:00
mcheping520
b5d3348d99 已拆分单个文件、翻译多国语言版本 2023-10-12 18:17:38 +08:00
Yifei Zhang
1e77df381a Update constant.ts 2023-10-12 17:31:29 +08:00
mcheping520
ec33281ff5 提供同步聊天记录教程,同时翻译多国语言 2023-10-11 21:29:50 +08:00
Yifei Zhang
ba67f13ad5 Merge pull request #2993 from H0llyW00dzZ/masks 2023-10-11 11:42:45 +08:00
H0llyW00dzZ
1604ed91fb Merge branch 'Yidadaa:main' into masks 2023-10-11 07:35:40 +07:00
H0llyW00dzZ
74fcaab5e9 Refactor Mask UI Page [Masks]
[+] refactor(mask.tsx): refactor simple search to be case-insensitive
2023-10-11 04:36:04 +07:00
Yifei Zhang
adee435ea6 Merge pull request #2992 from ograycode/patch-1 2023-10-11 00:42:52 +08:00
Jason O'Gray
ea59ab5176 Fix type in sync.ts
Simple typo fix.
2023-10-10 11:24:45 -04:00
Yifei Zhang
f78008c1b2 Merge pull request #2983 from Yidadaa/Yidadaa-patch-1 2023-10-10 11:56:24 +08:00
Yifei Zhang
f54db695af fix: #2981 full screen button not works 2023-10-10 11:54:25 +08:00
Yifei Zhang
f21f922160 Update tauri.conf.json 2023-10-09 22:46:36 +08:00
Yifei Zhang
fd413c7b52 Merge pull request #2947 from KeithHello/main 2023-10-09 22:45:56 +08:00
Yifei Zhang
3e2c5af4b5 Merge pull request #2975 from luckykong/patch-1 2023-10-09 22:38:45 +08:00
Kong Gaowen
bdb49b1171 修改界面
似乎这里引入了一个逻辑判断错误,会导致打包之后的界面变宽变高。
2023-10-09 18:50:10 +08:00
GH Action - Upstream Sync
5933b3d7eb Merge branch 'main' of https://github.com/Yidadaa/ChatGPT-Next-Web 2023-10-08 00:19:29 +00:00
Yifei Zhang
4c8d606fae Merge pull request #2950 from PeterDaveHello/locale-tw 2023-10-08 00:20:39 +08:00
Yifei Zhang
13c1d2fd2b Merge pull request #2946 from H0llyW00dzZ/ClientApp 2023-10-07 23:50:49 +08:00
Yifei Zhang
e35c807216 Update tauri.conf.json 2023-10-07 23:48:50 +08:00
Yifei Zhang
5a2cc6f154 Merge pull request #2927 from lifeeric/MACOS 2023-10-07 23:10:43 +08:00
Peter Dave Hello
88f8c43472 Improve tw locale 2023-10-07 23:06:20 +08:00
Yifei Zhang
ef3e8e6fac Merge pull request #2933 from H0llyW00dzZ/authpage 2023-10-07 23:03:56 +08:00
KeithHello
1505372e20 Change log
-   config.ts
    -   line 72: remove type confirmation of x as x always has type 'number'
    -   line 135: remove the redundant local variable
-   chat.ts
    -   delete the unused import
2023-10-04 18:08:29 +09:00
KeithHello
ad5093ce05 Change log
-   config.ts
    -   line 72 remove type confirmation of x as x always has type 'number'
    -   line 135 remove redundant local variable
-   chat.ts
    -   delete unused import
2023-10-04 17:40:23 +09:00
H0llyW00dzZ
b558d1afc6 Feat & Fix "Client App [Notification]"
[+] feat(update.ts): add support for localization in update notifications
[+] fix(update.ts): add missing semicolon in useUpdateStore function
2023-10-04 02:10:26 +07:00
H0llyW00dzZ
ddfd05b008 Fix & Feat Client App [Notification]
[+] fix(update.ts): remove unnecessary notification sending when permission is not granted
[+] feat(update.ts): add notification for already up to date version
2023-10-03 09:12:41 +07:00
H0llyW00dzZ
d2ad01a9ff Client App Fix Issue [Bug] 'export' button does not work #2884
[+] fix(exporter.tsx): add async keyword to download function
[+] feat(exporter.tsx): add support for saving image file using window.__TAURI__ API
[+] feat(global.d.ts): add types for window.__TAURI__ API methods
[+] feat(locales): add translations for download success and failure messages
[+] feat(sync.ts): add support for generating backup file name with date and time
[+] fix(utils.ts): add async keyword to downloadAs function and add support for saving file using window.__TAURI__ API
2023-10-03 08:49:03 +07:00
H0llyW00dzZ
64a17abfe2 Client App [Notification]
[+] feat(global.d.ts): add support for window.__TAURI__.notification methods
[+] feat(update.ts): add notification for new version availability
[+] fix(Cargo.toml): add tauri feature "notification-all" to enable notifications
[+] fix(tauri.conf.json): enable all notification features in tauri configuration
2023-10-03 08:08:11 +07:00
H0llyW00dzZ
04b638aa06 Fix & Refactor UI Page [Auth Page]
[+] fix(auth.tsx): fix conditional rendering of token input field
[+] refactor(auth.tsx): improve code readability by using conditional rendering for token input field
2023-10-01 00:23:19 +07:00
H0llyW00dzZ
31e30906d0 Refactor Locale Indonesia
[+] refactor(id.ts): remove unused import and isApp variable
[+] fix(id.ts): update Unauthorized error message
2023-09-30 22:59:24 +07:00
H0llyW00dzZ
bc00be9065 Feat & Fix UI Page [Auth Page]
[+] feat(auth.tsx): add goChat function to navigate to chat page
[+] fix(auth.tsx): change onClick event from goHome to goChat
2023-09-30 22:33:18 +07:00
H0llyW00dzZ
4a599e986f UI Page [Auth Page]
[+] feat(auth.tsx): add support for resetting access token in resetAccessCode function
[+] fix(auth.tsx): fix formatting issue in resetAccessCode function
[+] feat(locales): add support for sub tips in Auth component for multiple languages :
  - Add sub tips in Arabic locale (ar.ts)
  - Add sub tips in Bengali locale (bn.ts)
  - Add sub tips in Chinese locale (cn.ts)
  - Add sub tips in English locale (en.ts)
  - Add sub tips in Indonesian locale (id.ts)
2023-09-30 22:16:34 +07:00
EricGit
f1ca03e378 [FIXED] now it should detects all macintosh 2023-09-28 13:21:17 -04:00
Eric R
f3d5fc7a84 [FIXED] now the default key should be CMD on MacOS 2023-09-28 06:50:31 -04:00
Eric R
3bfcdf9c41 [ADDED] MacOS detect 2023-09-28 06:10:22 -04:00
Yifei Zhang
144200e315 Merge pull request #2912 from Algorithm5838/Algorithm5838-patch-1 2023-09-27 17:18:15 +08:00
Algorithm5838
398e229c77 Update chat.tsx 2023-09-26 21:49:17 +03:00
Algorithm5838
6a61fe5776 Update chat.tsx 2023-09-26 20:45:09 +03:00
Yifei Zhang
9835206452 Merge pull request #2905 from H0llyW00dzZ/ref#2895 2023-09-26 10:50:49 +08:00
H0llyW00dzZ
70b0580fb7 UI Page [sidebar]
[+] fix(sidebar.tsx): update onClick function to conditionally navigate to different paths based on config.dontShowMaskSplashScreen value

Ref : [Feature] Make the mask selection more streamlined Yidadaa#2895
2023-09-26 04:59:19 +07:00
Yifei Zhang
23eb7732d7 feat: support more http status check for webdav 2023-09-20 17:47:35 +08:00
Yifei Zhang
26e50cefea Update chat.tsx 2023-09-20 02:09:14 +08:00
Yifei Zhang
588e907181 Merge pull request #2873 from Yidadaa/bugfix-0919 2023-09-19 23:42:09 +08:00
Yidadaa
eae7d6260f fix: should not tight border in desktop app 2023-09-19 23:26:52 +08:00
Yifei Zhang
175b4e7f92 Update README_CN.md 2023-09-19 11:04:10 +08:00
Yifei Zhang
b050417ab1 Update tauri.conf.json 2023-09-19 11:03:22 +08:00
Yifei Zhang
37b49400db Update constant.ts 2023-09-19 11:03:03 +08:00
Yifei Zhang
ebcb2e7837 Merge pull request #2867 from Yidadaa/bugfix-0919 2023-09-19 03:34:45 +08:00
Yidadaa
f1e7db6a88 feat: auto fill upstash backup name 2023-09-19 03:33:17 +08:00
Yifei Zhang
2ba0929458 Merge pull request #2866 from Yidadaa/bugfix-0919 2023-09-19 03:20:37 +08:00
Yidadaa
83fed42997 feat: add upstash redis cloud sync 2023-09-19 03:18:34 +08:00
Yifei Zhang
2c4626709c Merge pull request #2865 from Yidadaa/bugfix-0919 2023-09-19 02:25:35 +08:00
Yidadaa
59fbadd9eb fixup 2023-09-19 02:21:31 +08:00
Yidadaa
adb860b464 fix: #2820 try to fix 520 error code 2023-09-19 02:12:43 +08:00
Yifei Zhang
8d8790586d Merge pull request #2864 from Yidadaa/bugfix-0919 2023-09-19 02:02:18 +08:00
Yidadaa
61ca60c550 fix: #2817 min-height for landscape orientation on mobile phone 2023-09-19 01:58:52 +08:00
Yidadaa
d713d01600 feat: close #2848 click drag icon to toggle sidebar width 2023-09-19 01:47:15 +08:00
Yifei Zhang
372ea0f845 Merge pull request #2853 from a6z6/main 2023-09-18 17:33:09 +08:00
Yifei Zhang
61888708f5 Merge pull request #2856 from koho/fix-font 2023-09-18 17:32:56 +08:00
Gerhard Tan
c900459f73 Encode google font url 2023-09-18 10:22:07 +08:00
Amor Zara
2c92f75c86 Update .env.template 2023-09-17 17:32:42 +08:00
Yifei Zhang
3e1514239c Merge pull request #2839 from a6z6/main 2023-09-15 15:17:46 +08:00
Yifei Zhang
0707a1d49a Merge pull request #2840 from Algorithm5838/Algorithm5838-patch-1 2023-09-15 15:17:35 +08:00
Amor Zara
9521f19507 Merge pull request #1 from a6z6/a6z6-patch-1
Update route.ts
2023-09-15 11:23:42 +08:00
Amor Zara
bd69116df2 Update route.ts
Correct typo error and make warning more specific.
2023-09-15 11:21:42 +08:00
Algorithm5838
6535986484 Update markdown.tsx 2023-09-15 06:06:34 +03:00
Yifei Zhang
e03db9c2d5 Merge pull request #2826 from H0llyW00dzZ/language 2023-09-14 10:49:44 +08:00
Yifei Zhang
038790370c Merge pull request #2822 from yhua1998/main 2023-09-13 22:40:03 +08:00
H0llyW00dzZ
261bf0b298 Changes "Nama" -> "User" 2023-09-13 20:49:22 +07:00
H0llyW00dzZ
5a7bdcfe59 [+] Some improvements to the Indonesian language. 2023-09-13 20:17:30 +07:00
H0llyW00dzZ
4f3261b262 [+] Some improvements to the Indonesian language. 2023-09-13 20:05:58 +07:00
yhua1998
48e6087b1b fix: The width of the sidebar changes abruptly by dragging it multiple times over and over again (bouncing) 2023-09-13 16:55:31 +08:00
yhua1998
28103c901d Refactor: sidebar drag 2023-09-13 16:24:21 +08:00
yhua1998
368701610f fix: Width changes abruptly when dragging the sidebar (jumps)
In useRef is non-responsive, we can't get the modified config.sidebarWidth dynamic modification value in handleMouseUp
2023-09-13 13:57:30 +08:00
Yifei Zhang
b589f48aa9 Update tauri.conf.json 2023-09-13 03:01:28 +08:00
Yifei Zhang
74e32efa7d Merge pull request #2819 from Yidadaa/webdav 2023-09-13 02:56:10 +08:00
Yidadaa
dc555b2206 fixup 2023-09-13 02:52:28 +08:00
Yidadaa
859cf6930f fixup 2023-09-13 02:51:57 +08:00
Yidadaa
6f83fbd212 feat: add webdav support 2023-09-13 02:51:02 +08:00
Yifei Zhang
a7be4780ba Merge pull request #2809 from kfear1337/auth-page 2023-09-12 18:14:13 +08:00
B0zal
b5e89d4440 [+] Updated Auth Page
- Made changes to the Auth Page to reset the input field for the access code when the "Later" button is clicked.
This ensures that only expected user is logged by entering access code or entering their OpenAI API Key, mitigating the risk of small bug issue
2023-09-12 07:00:23 +07:00
Yifei Zhang
89bc98d26b Merge pull request #2804 from Yidadaa/dependabot/npm_and_yarn/emoji-picker-react-4.5.1 2023-09-11 20:37:16 +08:00
Yifei Zhang
31a73273ac Merge pull request #2805 from Yidadaa/dependabot/npm_and_yarn/react-router-dom-6.15.0 2023-09-11 20:37:01 +08:00
Yifei Zhang
2a820ea6e7 Merge pull request #2808 from Yidadaa/dependabot/npm_and_yarn/eslint-8.49.0 2023-09-11 20:36:47 +08:00
Yifei Zhang
6484d4f0bd Merge pull request #2807 from Yidadaa/dependabot/npm_and_yarn/typescript-5.2.2 2023-09-11 20:36:33 +08:00
Yifei Zhang
461e48be32 Merge pull request #2799 from kfear1337/CodeQL-Report 2023-09-11 20:35:04 +08:00
dependabot[bot]
ff60ffca3e chore(deps-dev): bump eslint from 8.44.0 to 8.49.0
Bumps [eslint](https://github.com/eslint/eslint) from 8.44.0 to 8.49.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.44.0...v8.49.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-11 10:32:34 +00:00
dependabot[bot]
1bbf310c46 chore(deps-dev): bump typescript from 4.9.5 to 5.2.2
Bumps [typescript](https://github.com/Microsoft/TypeScript) from 4.9.5 to 5.2.2.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Commits](https://github.com/Microsoft/TypeScript/compare/v4.9.5...v5.2.2)

---
updated-dependencies:
- dependency-name: typescript
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-11 10:32:25 +00:00
dependabot[bot]
8469f448b5 chore(deps): bump react-router-dom from 6.14.1 to 6.15.0
Bumps [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) from 6.14.1 to 6.15.0.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@6.15.0/packages/react-router-dom)

---
updated-dependencies:
- dependency-name: react-router-dom
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-11 10:32:00 +00:00
dependabot[bot]
e36abc3ac6 chore(deps): bump emoji-picker-react from 4.4.8 to 4.5.1
Bumps [emoji-picker-react](https://github.com/ealush/emoji-picker-react) from 4.4.8 to 4.5.1.
- [Release notes](https://github.com/ealush/emoji-picker-react/releases)
- [Commits](https://github.com/ealush/emoji-picker-react/commits)

---
updated-dependencies:
- dependency-name: emoji-picker-react
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-11 10:31:50 +00:00
B0zal
605dd72354 [+] CodeQL Report Fix log injection vulnerability in useSyncStore
Severity : High
Sanitize the 'res' object before logging it in the 'check' method of useSyncStore to prevent log injection attacks.
The 'res' object is now sanitized by extracting only the necessary properties ('status', 'statusText', and 'headers') and logging the sanitized object instead.
This ensures that only safe and expected data is logged, mitigating the risk of log injection vulnerabilities.
2023-09-11 08:49:08 +07:00
Yifei Zhang
9770b65146 Merge pull request #2798 from Yidadaa/backup 2023-09-11 00:41:25 +08:00
Yidadaa
57158890c3 fixup 2023-09-11 00:39:56 +08:00
Yifei Zhang
6f8f490fdd Merge pull request #2797 from Yidadaa/backup 2023-09-11 00:36:24 +08:00
Yidadaa
415e9dc913 fixup: minor sync fixup 2023-09-11 00:34:51 +08:00
Yifei Zhang
1487762925 Merge pull request #2796 from Yidadaa/backup 2023-09-11 00:27:51 +08:00
Yidadaa
c73a91a0f5 fixup: fix type errors 2023-09-11 00:24:05 +08:00
Yidadaa
5dced28088 fixup: add en locales 2023-09-11 00:22:14 +08:00
Yidadaa
38f6956e71 feat: close #2754 add import/export to file 2023-09-11 00:20:23 +08:00
Yifei Zhang
93da9dd1f4 Merge pull request #2791 from shoito/patch-1 2023-09-10 21:47:17 +08:00
shoito
505c8cde81 improve japanese translations 2023-09-09 16:10:24 +09:00
Yifei Zhang
f7a6fa9873 Update README_CN.md 2023-09-07 17:43:17 +08:00
Yifei Zhang
70a2d11b8c Merge pull request #2781 from rickyrobinett/main 2023-09-07 10:40:26 +08:00
Ricky Robinett
2c077aca5a fix cloudflare deployment instructions 2023-09-06 15:36:12 -04:00
Yifei Zhang
efef6b098d Merge pull request #2764 from Yidadaa/bugfix-0905 2023-09-05 02:10:53 +08:00
Yidadaa
885f2a3226 feat: close #2752 auto re-fill unfinished input 2023-09-05 01:54:28 +08:00
Yifei Zhang
1f94ae7888 Merge pull request #2721 from Yidadaa/revert-2673-dependabot/npm_and_yarn/next-13.4.19 2023-08-28 11:10:34 +08:00
Yifei Zhang
05aa520669 Revert "chore(deps): bump next from 13.4.9 to 13.4.19" 2023-08-28 11:06:12 +08:00
Yifei Zhang
b0c32159e7 Update tauri.conf.json 2023-08-28 01:15:46 +08:00
Yifei Zhang
4a182517da Update README.md 2023-08-28 01:04:24 +08:00
Yifei Zhang
107b584085 Merge pull request #2705 from kfear1337/language 2023-08-28 01:02:23 +08:00
Yifei Zhang
6d8416f838 Merge pull request #2713 from kfear1337/CodeQL-Report 2023-08-28 00:54:58 +08:00
Yifei Zhang
876a0cb6f7 Merge pull request #2717 from Yidadaa/bugfix-0827 2023-08-28 00:51:54 +08:00
Yidadaa
4904612523 feat: close #2430 add a simple user maual 2023-08-28 00:50:53 +08:00
Yidadaa
3bd76b9156 feat: close #2580 only use 3.5 to summarize when not using custom models 2023-08-28 00:02:52 +08:00
Yifei Zhang
dd757e7d04 Merge pull request #2711 from huhuhuni/main 2023-08-27 19:33:03 +08:00
B0zal
71d06647d0 Delete codeql.yml
it won't work if someone try, must have licence advance security
2023-08-27 09:15:05 +07:00
B0zal
0ae57589a9 This branch only for scanner 2023-08-27 06:09:55 +07:00
B0zal
22a6819f7b Security Update
[+] Protect Prototype
2023-08-26 22:16:29 +07:00
B0zal
5e23ad2db1 Security Update
Potentially unsafe external link
2023-08-26 22:16:29 +07:00
Yifei Zhang
e009ea57b9 Merge pull request #2706 from kfear1337/bug 2023-08-26 19:58:46 +08:00
B0zal
63c93a42b5 [+] Fixed language missing for finetuned 2023-08-25 18:15:50 +07:00
B0zal
e6cc1625b5 [+] FineTuned Sysmessage Depends for local language
this for JSON Exporter
2023-08-25 17:48:25 +07:00
B0zal
f65b0128e7 Issue #2702 should be fixed now kiw kiw
where the ChatGPTicon.src
glitches and breaks when exporting the image for the second time without refreshing the page.
2023-08-25 17:46:19 +07:00
B0zal
507b7fee56 [+] Language indonesia (my country) 2023-08-25 17:42:02 +07:00
huni
925d28495a fix bug of ! near check of system. 2023-08-24 23:24:26 +08:00
huni
19dd71eb05 setup shell support for debian 2023-08-24 22:04:42 +08:00
Yifei Zhang
b3fd56c2c1 Merge pull request #2685 from reece00/456 2023-08-24 11:12:47 +08:00
Yifei Zhang
d8b6ebf6cb fix: #2699 remove double quotes in readme 2023-08-24 11:09:17 +08:00
Yifei Zhang
bdaac17886 Merge pull request #2698 from kfear1337/main 2023-08-24 10:58:47 +08:00
Yifei Zhang
8cac51abbe chore: #2699 Update README.md 2023-08-24 10:54:28 +08:00
B0zal
0113d4499b [Feature] Better JSON Exporter #2692
[+] A view looks better
[+] auto minify json when click a copy in markdown and download

Co-Authored-By: wangwentong-lunaon <39506652+wangwentong-lunaon@users.noreply.github.com>
Co-Authored-By: Yifei Zhang <yidadaa@qq.com>
Co-Authored-By: B0zal <48602426+kfear1337@users.noreply.github.com>
2023-08-23 21:14:43 +07:00
Yifei Zhang
7562ab3c1c Merge pull request #2690 from pengooseDev/main 2023-08-23 19:06:01 +08:00
pengoosedev
d3de07ecf3 docs: Add vercel-ko 2023-08-23 01:33:46 +09:00
pengoosedev
b146cd889f Add faq-ko 2023-08-23 01:28:54 +09:00
pengoosedev
bc0f184098 docs: Add cloudflare-pages-ko 2023-08-23 01:08:31 +09:00
pengoosedev
1debde3046 docs: Add README_KO 2023-08-23 00:08:08 +09:00
reece00
4d3fdbdd80 The mobile terminal ishitbottom does not perform -10 2023-08-22 12:35:51 +08:00
Yifei Zhang
90c201d438 Merge pull request #2673 from Yidadaa/dependabot/npm_and_yarn/next-13.4.19 2023-08-22 11:43:59 +08:00
Yifei Zhang
1e22abab80 Merge pull request #2674 from Yidadaa/dependabot/npm_and_yarn/eslint-config-next-13.4.19 2023-08-22 11:43:38 +08:00
Yifei Zhang
792c37735e Merge pull request #2675 from Yidadaa/dependabot/npm_and_yarn/mermaid-10.3.1 2023-08-22 11:43:26 +08:00
Yifei Zhang
6d8ece530f Merge pull request #2677 from Yidadaa/dependabot/npm_and_yarn/prettier-3.0.2 2023-08-22 11:43:06 +08:00
dependabot[bot]
3499dfb285 chore(deps-dev): bump prettier from 2.8.8 to 3.0.2
Bumps [prettier](https://github.com/prettier/prettier) from 2.8.8 to 3.0.2.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/2.8.8...3.0.2)

---
updated-dependencies:
- dependency-name: prettier
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-21 10:57:49 +00:00
dependabot[bot]
30c656ceda chore(deps): bump mermaid from 10.2.3 to 10.3.1
Bumps [mermaid](https://github.com/mermaid-js/mermaid) from 10.2.3 to 10.3.1.
- [Release notes](https://github.com/mermaid-js/mermaid/releases)
- [Changelog](https://github.com/mermaid-js/mermaid/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/mermaid-js/mermaid/compare/v10.2.3...v10.3.1)

---
updated-dependencies:
- dependency-name: mermaid
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-21 10:57:23 +00:00
dependabot[bot]
a1c7f86ff3 chore(deps-dev): bump eslint-config-next from 13.2.3 to 13.4.19
Bumps [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) from 13.2.3 to 13.4.19.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/commits/v13.4.19/packages/eslint-config-next)

---
updated-dependencies:
- dependency-name: eslint-config-next
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-21 10:57:07 +00:00
dependabot[bot]
4952e41132 chore(deps): bump next from 13.4.9 to 13.4.19
Bumps [next](https://github.com/vercel/next.js) from 13.4.9 to 13.4.19.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v13.4.9...v13.4.19)

---
updated-dependencies:
- dependency-name: next
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-21 10:56:27 +00:00
Yifei Zhang
e1142216ec fix: #2672 should use correct resend index 2023-08-21 18:33:45 +08:00
Yifei Zhang
a4040fc1ee Update README_CN.md 2023-08-21 18:30:44 +08:00
Yifei Zhang
f84572443f Update faq-cn.md 2023-08-21 16:00:19 +08:00
Yifei Zhang
fbf6bc1979 Merge pull request #2655 from ClarenceDan/main 2023-08-19 18:01:09 +08:00
GH Action - Upstream Sync
3aeb2c1230 Merge branch 'main' of https://github.com/Yidadaa/ChatGPT-Next-Web 2023-08-19 01:06:19 +00:00
Yifei Zhang
50eb7a5f98 Update tauri.conf.json 2023-08-18 18:47:54 +08:00
Yifei Zhang
484d1e7396 Merge pull request #2649 from pichuang/zh-tw 2023-08-18 18:40:30 +08:00
Yifei Zhang
a632e13334 Merge pull request #2653 from Algorithm5838/Algorithm5838-patch-1 2023-08-18 18:40:18 +08:00
Clarence Dan
aa3f96f89c Update globals.scss 2023-08-18 18:18:16 +08:00
Clarence Dan
16685ddb6c Update chat.module.scss 2023-08-18 17:21:58 +08:00
Clarence Dan
e78b15b9f0 Update chat.module.scss
Specify styles for iOS devices.
2023-08-18 17:12:02 +08:00
Algorithm5838
35b0bd76f8 Update markdown.tsx 2023-08-18 08:12:27 +03:00
Phil Huang
1d7286c161 Improve the text in tw.ts
Adoption of text that is more closely aligned with the usage of zh-tw
2023-08-17 23:50:38 +08:00
Yifei Zhang
5a7ec38ecd Update README.md 2023-08-16 11:22:51 +08:00
Yifei Zhang
ada4e3cdcd Merge pull request #2641 from Yidadaa/bugfix-0815 2023-08-15 23:09:15 +08:00
Yidadaa
ed62c87156 feat: close #2638 hide auth page and use better unauth tips 2023-08-15 22:50:42 +08:00
Yidadaa
840277f584 fix: #2566 click avatar to edit context messages 2023-08-15 22:42:55 +08:00
Yifei Zhang
ffddb7fb2b Merge pull request #2636 from wangwentong-lunaon/main 2023-08-15 22:09:14 +08:00
wangwentong
b380421fd5 support json export 2023-08-15 13:32:34 +08:00
Yifei Zhang
20882a7598 Merge pull request #2629 from Yidadaa/bugfix-0814 2023-08-14 22:13:56 +08:00
Yifei Zhang
9d3dff47d2 Merge pull request #2627 from imldy/dev_title-generation-checkbox 2023-08-14 22:12:26 +08:00
Yidadaa
db5c7aba78 fix: #2615 scrollbar jitter under certain message counts 2023-08-14 22:11:38 +08:00
Yidadaa
e8e01aa60d feat: close #2618 use correct html lang attr 2023-08-14 21:55:18 +08:00
Yidadaa
ae8226907f feat: close #2621 use better default api url 2023-08-14 21:36:29 +08:00
imldy
803b66ae9d chore: Concise description 2023-08-14 20:47:02 +08:00
imldy
20a508e2d6 feat: add autoGenerateTitle option
(cherry picked from commit 656ab94a9c4edfee820616b8cfc39f5ee9952a3a)
2023-08-14 20:42:09 +08:00
Yifei Zhang
25ada1eae9 Merge pull request #2617 from Yidadaa/rtl-fix 2023-08-14 11:13:56 +08:00
Yifei Zhang
808e4b38a3 fixup 2023-08-14 11:10:02 +08:00
Yifei Zhang
a496bc5a63 fix: #2614 better rtl detecting algo 2023-08-14 10:57:24 +08:00
Yifei Zhang
c94713475f Update globals.scss 2023-08-13 02:47:07 +08:00
Yifei Zhang
d45c61109f Merge pull request #2607 from eltociear/add_ja-docs 2023-08-13 00:07:37 +08:00
Ikko Eltociear Ashimine
99220d72da Add faq-ja.md 2023-08-11 20:28:18 +09:00
Ikko Eltociear Ashimine
887eaef1aa Add vercel-ja.md 2023-08-11 19:58:54 +09:00
Ikko Eltociear Ashimine
836a00e104 Add cloudflare-pages-ja.md 2023-08-11 19:49:36 +09:00
Ikko Eltociear Ashimine
8ee5061046 Add Japanese README 2023-08-11 19:34:21 +09:00
Yifei Zhang
61852b4021 Merge pull request #2598 from Yidadaa/Yidadaa-patch-1 2023-08-10 10:49:16 +08:00
Yifei Zhang
0b7de6f7b2 fix: #2594 trim the / 2023-08-10 10:47:06 +08:00
Yifei Zhang
9834a67cbd Update tauri.conf.json 2023-08-09 15:37:13 +08:00
Yifei Zhang
d85a4a0c9f Merge pull request #2590 from ZhangYichi-ZYc/ZhangYichi-ZYc-patch-1 2023-08-09 15:29:46 +08:00
Yifei Zhang
67c8ec6d7e chore: change ACCESS_CODE_PREFIX to nk- 2023-08-09 15:27:08 +08:00
Yifei Zhang
2a2dd7ea19 Merge pull request #2588 from eltociear/eltociear-patch-1
Fix typo in README.md
2023-08-09 09:17:55 +08:00
Ikko Eltociear Ashimine
153e7ac7e4 Fix typo in README.md
notifictions -> notifications
2023-08-09 01:22:48 +09:00
Yifei Zhang
9420fd4946 Merge pull request #2585 from Yidadaa/bugfix-0808 2023-08-08 21:38:42 +08:00
Yidadaa
b14c5cd89c fix: #2485 one-time-use body 2023-08-08 21:36:37 +08:00
Yidadaa
4ab9141429 fix: #2564 should not clear message when error 2023-08-08 21:24:45 +08:00
Yidadaa
769c2f9f49 feat: close #2583 do not summarize with gpt-4 2023-08-08 21:22:41 +08:00
Yifei Zhang
c41c498a9c Merge pull request #2573 from 7lsu/main 2023-08-08 16:00:45 +08:00
7lsu
d1096582a5 font family display enhance 2023-08-07 17:44:32 +08:00
Yifei Zhang
543989151f Update constant.ts 2023-08-04 19:24:10 +08:00
Yifei Zhang
7da83987e4 chore: use bigger page size and render msg count 2023-08-04 15:45:16 +08:00
Yidadaa
523d553dac fix: clear btn should display in correct place 2023-08-04 02:58:02 +08:00
Yifei Zhang
ff6f0e9546 Merge pull request #2557 from Yidadaa/bugfix-0804
feat: disable auto focus on mobile screen
2023-08-04 02:45:19 +08:00
Yidadaa
3e63f6ba34 feat: disable auto focus on mobile screen 2023-08-04 02:43:55 +08:00
Yifei Zhang
811b92d24e Merge pull request #2556 from Yidadaa/bugfix-0804
fixup: improve auto scroll algo
2023-08-04 02:40:34 +08:00
Yidadaa
bc5ddc4541 fixup: improve auto scroll algo 2023-08-04 02:39:32 +08:00
Yifei Zhang
44e43729bf Merge pull request #2555 from Yidadaa/bugfix-0804
feat: close #2545 improve lazy load message list
2023-08-04 02:18:55 +08:00
Yidadaa
203067c936 feat: close #2545 improve lazy load message list 2023-08-04 02:16:44 +08:00
Zhang Yichi
f3b508c088 Update Model Pricing.md
OpenAI has updated their model prices, reducing the input of GPT-3.5 to $0.0015/1000 tokens
2023-08-03 23:52:18 +08:00
Yifei Zhang
081d84f848 Update tauri.conf.json 2023-08-03 10:41:45 +08:00
Yifei Zhang
e381add944 Merge pull request #2542 from Yidadaa/yifei-bugfix-0802
chore: smaller auto scroll interval
2023-08-02 23:54:19 +08:00
Yidadaa
75d4eca722 chore: smaller auto scroll interval 2023-08-02 23:51:01 +08:00
Yifei Zhang
56904ad6b2 Merge pull request #2541 from Yidadaa/yifei-bugfix-0802
feat: improve chat action, auto scroll and edit model title
2023-08-02 23:40:50 +08:00
Yidadaa
b5ef552c25 feat: improve auto scroll ux and edit model title 2023-08-02 23:35:51 +08:00
Yidadaa
cbabb9392c feat: improve ChatAction ux 2023-08-02 22:53:36 +08:00
Yifei Zhang
531b3dcf9e Merge pull request #2531 from LeviBorodenko/main
Add link to authentication page to i18n
2023-08-02 15:35:05 +08:00
Levi Borodenko
d975daf3f0 locale: add link to authentication page in i18n 2023-08-01 16:17:26 +02:00
Yifei Zhang
6137d551fe Merge pull request #2525 from 7lsu/main
typo fix
2023-08-01 15:17:26 +08:00
7lsu
cf625e3542 typo fix 2023-08-01 10:16:36 +08:00
Yifei Zhang
e354fca4a4 Merge pull request #2524 from Yidadaa/bugfix-0731
fix: #2514 should not clear the message after editing message
2023-07-31 22:23:11 +08:00
Yidadaa
129e7afc16 fix: #2514 should not clear the message after editing message 2023-07-31 22:20:39 +08:00
Yifei Zhang
e83e0f6a33 Merge pull request #2486 from fernandoxu/main
fix(typo): ngnix -> nginx
2023-07-26 17:13:46 +08:00
fernandoxu
cf4f928b25 fix(typo): ngnix -> nginx 2023-07-26 10:06:06 +08:00
Yifei Zhang
e4d955e3f9 Merge pull request #2483 from Yidadaa/bugfix-0725
fix: #2393 try to fix chat list lag
2023-07-25 23:01:46 +08:00
Yidadaa
13576087f4 fix: #2393 try to fix chat list lag 2023-07-25 22:59:21 +08:00
Yifei Zhang
e74f6f3183 Merge pull request #2469 from Yidadaa/dependabot/npm_and_yarn/types/react-dom-18.2.7
chore(deps-dev): bump @types/react-dom from 18.0.11 to 18.2.7
2023-07-24 22:06:40 +08:00
dependabot[bot]
8302d1d3c1 chore(deps-dev): bump @types/react-dom from 18.0.11 to 18.2.7
Bumps [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) from 18.0.11 to 18.2.7.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-24 10:33:12 +00:00
Yifei Zhang
b7320e6834 Update tauri.conf.json 2023-07-21 00:37:38 +08:00
Yifei Zhang
3193fe0235 Merge pull request #2452 from Yidadaa/bugfix-0720
feat: close #2449 edit / insert / delete messages modal
2023-07-21 00:25:33 +08:00
Yidadaa
7c2fa9f8a4 feat: close #2449 edit / insert / delete messages modal 2023-07-21 00:24:26 +08:00
Yidadaa
e5f6133127 feat: close #2447 pre-fill key/code/url 2023-07-20 23:17:58 +08:00
Yidadaa
0198c5b781 feat: close #2445 switch to mit license 2023-07-20 22:51:49 +08:00
Yifei Zhang
f535073ae4 Merge pull request #2431 from Hopelwj/main
设置页面-参数过滤空格
2023-07-19 20:14:13 +08:00
Hopelwj
b9895ecadd Merge branch 'Yidadaa:main' into main 2023-07-18 19:39:59 +08:00
liuweijie
322eb66fdf fix: useAccessStore filter spaces 2023-07-18 19:38:16 +08:00
Yifei Zhang
f0abdc80eb Update tauri.conf.json 2023-07-18 10:50:44 +08:00
Yifei Zhang
70a4f94c81 Update home.tsx 2023-07-17 11:33:19 +08:00
Yifei Zhang
f7e4b36746 Merge pull request #2412 from Yidadaa/fix-2409
fix: #2409 empty prompt id cause crash
2023-07-17 11:03:33 +08:00
Yifei Zhang
4e6b71ace5 Merge pull request #2410 from Lantianyou/feature/typing-metadata
Feature/typing-metadata
2023-07-17 11:01:39 +08:00
Yifei Zhang
c916cd1a87 Update mask.tsx 2023-07-17 11:00:45 +08:00
兰天游
442a529a72 feat: add type for import 2023-07-16 21:35:13 +08:00
兰天游
e1243f3d59 feat: add typings for metadata 2023-07-16 21:34:01 +08:00
Yifei Zhang
62f8cd1db6 Merge pull request #2406 from Yidadaa/bugfix-0716
feat: close #2376 add babel polyfill
2023-07-16 16:34:45 +08:00
Yidadaa
af5f67d459 feat: close #2376 add babel polyfill 2023-07-16 16:32:22 +08:00
Yifei Zhang
13424a893e Merge pull request #2405 from Yidadaa/bugfix-0716
fix: #2367 do not copy in async callback after sharing to ShareGPT
2023-07-16 16:17:40 +08:00
Yidadaa
30473ec41e fix: #2367 do not copy in async callback after sharing to ShareGPT 2023-07-16 16:14:57 +08:00
Yifei Zhang
24d382c70d Merge pull request #2404 from Yidadaa/bugfix-0716
feat: improve dnd icon
2023-07-16 16:14:25 +08:00
Yidadaa
3ddedc903e feat: improve dnd icon 2023-07-16 15:49:15 +08:00
Yifei Zhang
e3c279b8be Merge pull request #2389 from MarkShawn2020/fix-default-openai-url
fixed openai base url if empty
2023-07-16 15:20:26 +08:00
Yifei Zhang
d909b676c5 Update README.md 2023-07-16 15:08:53 +08:00
Yifei Zhang
08a8ac9971 Merge pull request #2388 from yikayiyo/feat-drag-prompts
feat: #2297 drag and drop in contextual prompts
2023-07-16 14:30:49 +08:00
Yifei Zhang
7073fd2f3b Merge pull request #2392 from imldy/dev_SetInjectSystemPrompts
fix: enable `enableInjectSystemPrompts` attribute for old sessions
2023-07-16 14:29:50 +08:00
imldy
a9f67a48a1 dev: use current inject configuration 2023-07-15 02:48:47 +08:00
imldy
fd058cc693 fix: enable enableInjectSystemPrompts attribute for old sessions 2023-07-15 01:32:39 +08:00
Yifei Zhang
c98df33003 Merge pull request #2390 from MarkShawn2020/fix-react-key
fixed react key
2023-07-15 01:28:56 +08:00
markshawn2020
3f8b14f5f8 fixed react key 2023-07-14 21:15:34 +08:00
markshawn2020
1c335a68e0 fixed openai base url if empty 2023-07-14 21:03:32 +08:00
legao
fb98050d9f feat: drag and drop in contextual prompts 2023-07-14 18:08:03 +08:00
Yifei Zhang
91cb7aa13d Merge pull request #2372 from Yidadaa/dependabot/npm_and_yarn/semver-6.3.1
chore(deps): bump semver from 6.3.0 to 6.3.1
2023-07-13 16:46:00 +08:00
dependabot[bot]
ab0f7cc0c9 chore(deps): bump semver from 6.3.0 to 6.3.1
Bumps [semver](https://github.com/npm/node-semver) from 6.3.0 to 6.3.1.
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/v6.3.1/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v6.3.0...v6.3.1)

---
updated-dependencies:
- dependency-name: semver
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-13 01:58:09 +00:00
Yifei Zhang
b51f7f9a25 Merge pull request #2370 from Yidadaa/bugfix-0712
fix: sharegpt roles
2023-07-12 23:21:22 +08:00
Yidadaa
d275e32e70 fix: sharegpt roles 2023-07-12 23:19:46 +08:00
Yifei Zhang
22cf78d506 Merge pull request #2355 from fyl080801/main
fix: cloudflare headers
2023-07-11 23:57:03 +08:00
Yifei Zhang
c7c318b31e Merge pull request #2360 from imldy/fix_can't_edit_old_mask
fix: 迁移面具对象的key以解决无法编辑旧面具的问题
2023-07-11 23:48:02 +08:00
imldy
a4d012828c fix: migrated mask object key 2023-07-11 21:02:09 +08:00
yuanliang feng
72cfebd2bc Merge pull request #3 from fyl080801/deploy
Deploy
2023-07-11 17:08:50 +08:00
yuanliang feng
a832cfb343 Update common.ts 2023-07-11 15:46:40 +08:00
yuanliang feng
9ef680db57 Merge pull request #2 from fyl080801/main
update
2023-07-10 23:35:19 +08:00
yuanliang feng
2c09f4ef90 Merge pull request #1 from fyl080801/main
update
2023-07-10 23:23:20 +08:00
Yifei Zhang
c44454bf74 Merge pull request #2346 from Yidadaa/bugfix-0710
fix: #2336 resending message should delete origional messages
2023-07-10 23:22:53 +08:00
Yidadaa
ad1b9b7f6d fixup 2023-07-10 23:21:22 +08:00
Yidadaa
15e063e1b5 feat: #2330 disable /list/models 2023-07-10 23:19:43 +08:00
Yidadaa
c00a63e4c3 fix: #2336 resending message should delete origional messages 2023-07-10 22:59:12 +08:00
Yifei Zhang
dbda27b1d6 Merge pull request #2343 from Yidadaa/dependabot/npm_and_yarn/next-13.4.9
chore(deps): bump next from 13.4.6 to 13.4.9
2023-07-10 21:52:19 +08:00
Yifei Zhang
9eef879dbe Merge pull request #2342 from Yidadaa/dependabot/npm_and_yarn/tauri-apps/cli-1.4.0
chore(deps-dev): bump @tauri-apps/cli from 1.3.1 to 1.4.0
2023-07-10 21:52:08 +08:00
Yifei Zhang
cb29423636 Merge pull request #2341 from Yidadaa/dependabot/npm_and_yarn/types/react-18.2.14
chore(deps-dev): bump @types/react from 18.2.12 to 18.2.14
2023-07-10 21:51:56 +08:00
Yifei Zhang
d754eecf07 Merge pull request #2340 from Yidadaa/dependabot/npm_and_yarn/eslint-8.44.0
chore(deps-dev): bump eslint from 8.37.0 to 8.44.0
2023-07-10 21:51:45 +08:00
Yifei Zhang
1e308782ee Merge pull request #2339 from Yidadaa/dependabot/npm_and_yarn/react-router-dom-6.14.1
chore(deps): bump react-router-dom from 6.10.0 to 6.14.1
2023-07-10 21:51:32 +08:00
Yifei Zhang
69139cd85b Merge pull request #2344 from imldy/dev_SetInjectSystemPrompts
Dev: 给是否注入全局系统prompt提供一个开关
2023-07-10 21:51:06 +08:00
imldy
f59235bd5a feat: 翻译InjectSystemPrompts配置项为其他语言 2023-07-10 20:14:10 +08:00
imldy
2930ba0457 fix: 修改InjectSystemPrompts.SubTitle使其更符合系统行为 2023-07-10 20:12:51 +08:00
imldy
1513881eed fix: 修改是否应该注入System Prompt的判断规则为根据设置项 2023-07-10 18:57:54 +08:00
imldy
5e361f6748 dev: 增加模型设置项:是否启用注入全局 System Prompt 功能 2023-07-10 18:56:22 +08:00
dependabot[bot]
b182543a21 chore(deps): bump next from 13.4.6 to 13.4.9
Bumps [next](https://github.com/vercel/next.js) from 13.4.6 to 13.4.9.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v13.4.6...v13.4.9)

---
updated-dependencies:
- dependency-name: next
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-10 10:51:20 +00:00
dependabot[bot]
cbce8b444f chore(deps-dev): bump @tauri-apps/cli from 1.3.1 to 1.4.0
Bumps [@tauri-apps/cli](https://github.com/tauri-apps/tauri) from 1.3.1 to 1.4.0.
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/cli.js-v1.3.1...@tauri-apps/cli-v1.4.0)

---
updated-dependencies:
- dependency-name: "@tauri-apps/cli"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-10 10:48:41 +00:00
dependabot[bot]
38e92cfc62 chore(deps-dev): bump @types/react from 18.2.12 to 18.2.14
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 18.2.12 to 18.2.14.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-10 10:47:57 +00:00
dependabot[bot]
a7764dc6d5 chore(deps-dev): bump eslint from 8.37.0 to 8.44.0
Bumps [eslint](https://github.com/eslint/eslint) from 8.37.0 to 8.44.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.37.0...v8.44.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-10 10:47:33 +00:00
dependabot[bot]
c74552c4c5 chore(deps): bump react-router-dom from 6.10.0 to 6.14.1
Bumps [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) from 6.10.0 to 6.14.1.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@6.14.1/packages/react-router-dom)

---
updated-dependencies:
- dependency-name: react-router-dom
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-10 10:46:56 +00:00
yuanliang feng
7a0b437626 Update common.ts 2023-07-10 17:26:28 +08:00
Yifei Zhang
c7e9f13d2e Update tauri.conf.json 2023-07-10 15:03:02 +08:00
Yifei Zhang
51e816ad6d Merge pull request #2320 from yanCode/main
fix: typos
2023-07-10 14:25:47 +08:00
ShengYan, Zhang
dd047fd58f fix: typos 2023-07-10 10:09:19 +08:00
Yifei Zhang
300f023d2a Merge pull request #2318 from Yidadaa/bugfix-0709-2
chore: suppress module not found warning
2023-07-09 22:23:43 +08:00
Yidadaa
a282a80a6e chore: suppress module not found warning 2023-07-09 22:18:48 +08:00
Yifei Zhang
e668e17655 Merge pull request #2317 from Yidadaa/bugfix-0709-2
fix: #2303 should be able to select custom models
2023-07-09 22:12:04 +08:00
Yidadaa
a913d9728c fixup 2023-07-09 22:06:41 +08:00
Yifei Zhang
15862d393b Merge pull request #2316 from SergeWilfried/main
Add Missing FR Translations
2023-07-09 22:01:23 +08:00
Yidadaa
48a6cdd50a feat: improve model selector ui 2023-07-09 22:01:04 +08:00
Yidadaa
09b05cde7f fix: #2303 should select custom models 2023-07-09 21:56:49 +08:00
SergeWilfried
da294038a8 Merge pull request #2 from Yidadaa/main
Upstream Sync
2023-07-09 13:21:05 +00:00
Yifei Zhang
332f3fb8e8 Merge pull request #2314 from Yidadaa/bugfix-0709-2
feat: #920  migrate id to nanoid
2023-07-09 19:42:06 +08:00
Yidadaa
8e4743e719 feat: #920 migrate id to nanoid 2023-07-09 19:37:42 +08:00
Yifei Zhang
4a663e539a Merge pull request #2312 from Yidadaa/bugfix-0709-2
feat: close #2303 add custom model name config
2023-07-09 18:17:38 +08:00
Yidadaa
98ac7ee277 feat: close #2303 add custom model name config 2023-07-09 18:15:52 +08:00
Yidadaa
28c457730a fix: #2280 auto-detect models from 'list/models' 2023-07-09 18:03:06 +08:00
Yifei Zhang
9b61fe9df2 Merge pull request #2309 from Yidadaa/bugfix-0709
feat: #2308 #2294 #2295
2023-07-09 16:50:00 +08:00
Yidadaa
b55b01cb13 feat: #2308 improve chat actions ux 2023-07-09 16:39:46 +08:00
Yidadaa
90d8f3117f fix: #2295 use correct methods to migrate state 2023-07-09 16:28:15 +08:00
Yidadaa
c7e976c8c5 chore: update readme 2023-07-09 16:26:18 +08:00
Yidadaa
6014b765f4 feat: close #2294 add documents for adding a new translation 2023-07-09 16:26:00 +08:00
Yidadaa
ca295588c4 fix: #2308 improve chat actions 2023-07-09 16:15:58 +08:00
serge
437334355f chore(fr.ts): update French translations for improved user experience and clarity
feat(fr.ts): add new translations for pinning messages, editing, and input actions
feat(fr.ts): add new translations for commands and input actions related to conversation management
feat(fr.ts): add new translations for settings options related to resetting and clearing data
feat(fr.ts): add new translations for settings options related to language, font size, and input template
feat(fr.ts): add new translations for settings options related to update version and presence penalty
feat(fr.ts): add new translations for settings options related to masking and built-in assistants
feat(fr.ts): add new translations for settings options related to temperature and top P
feat(fr.ts): add new translations for settings options related to max tokens and presence penalty
feat(fr.ts): add new translations for settings options related to cloning and configuration
feat(fr.ts): add new translations for settings options related to avatar, name, sync, hide context, and sharing
2023-07-08 14:12:46 +00:00
Yifei Zhang
3432d4df29 Update README.md 2023-07-07 17:57:53 +08:00
Yifei Zhang
05fcb96181 Merge pull request #2275 from Saber-Kurama/pref/api-models
perf:  models接口返回异常数据的容错处理
2023-07-07 11:23:30 +08:00
Yifei Zhang
6bb27f90a4 Merge pull request #2277 from PaRaD1SE98/main
fix: selector z-index bug
2023-07-07 11:16:22 +08:00
PaRaD1SE98
c10e8382a9 fix: selector z-index bug 2023-07-07 04:15:05 +09:00
guochao
6653a31eb7 perf: models接口返回数据的容错处理 2023-07-06 20:00:30 +08:00
Yifei Zhang
42561d04de Update tauri.conf.json 2023-07-06 11:21:55 +08:00
Yidadaa
9a285ab935 fixup 2023-07-06 02:12:37 +08:00
Yifei Zhang
81771568be Merge pull request #2269 from Yidadaa/bugfix-0705
feat: improve image preview & model switcher model ux
2023-07-06 02:09:05 +08:00
Yifei Zhang
795b43f174 Merge pull request #2268 from imabutahersiddik/main
Added support for Bangla language.
2023-07-06 02:08:03 +08:00
Yidadaa
52203b50eb fixup 2023-07-06 02:07:35 +08:00
Yidadaa
0373b2c9dd feat: close #2266 use modal to switch model 2023-07-06 02:03:31 +08:00
ki-ask
fe2c1c4ec6 Added support for Bangla language 2023-07-05 23:55:43 +06:00
ki-ask
596b6542e8 Added support for Bangla language 2023-07-05 23:55:14 +06:00
ki-ask
6897bf1254 Added support for Bangla language 2023-07-05 23:39:22 +06:00
ki-ask
8a6a13e583 Added support for Bangla language 2023-07-05 23:35:55 +06:00
Yidadaa
b718285125 feat: improve svg viewer 2023-07-06 01:33:30 +08:00
ki-ask
e5ab918ef9 Added support for Bangla language 2023-07-05 23:31:45 +06:00
Yidadaa
6c6a2d08db feat: close #2267 display a modal to export image 2023-07-06 01:26:06 +08:00
Yidadaa
9e6617e3ca feat: add max icon for modals 2023-07-06 01:11:50 +08:00
Yifei Zhang
94a50f92e1 Merge pull request #2264 from Yidadaa/bugfix-0705
feat: close #1072 share mask as link
2023-07-05 23:21:49 +08:00
Yidadaa
5c8be2a8f6 feat: close #1072 share mask as link 2023-07-05 23:19:54 +08:00
Yifei Zhang
1dcf2d80b4 Merge pull request #2263 from Yidadaa/bugfix-0705
fix: #2252 polyfill composing for old safari browsers & fix #2261
2023-07-05 22:51:49 +08:00
Yidadaa
1197521921 fix: #2252 polyfill composing for old safari browsers 2023-07-05 22:50:12 +08:00
Yidadaa
3863cfe786 fix: #2261 default enable gpt-4 models 2023-07-05 22:41:10 +08:00
Yidadaa
54bd07702c refactor: #2256 hide builtin masks 2023-07-05 22:39:25 +08:00
Yifei Zhang
a75e2b0c0e Merge pull request #2256 from marcusschiesser/main
feat: add setting to hide built-in masks
2023-07-05 22:30:28 +08:00
Yifei Zhang
29fd9b23fe Merge pull request #2260 from sunls233/fix-summarize
修复摘要总结后不会被使用
2023-07-05 22:28:03 +08:00
sunls233
089e3b8946 fix summarize don't send 2023-07-05 14:27:37 +08:00
GH Action - Upstream Sync
9c36fcec81 Merge branch 'main' of https://github.com/Yidadaa/ChatGPT-Next-Web 2023-07-05 00:12:23 +00:00
Marcus Schiesser
74fa065266 feat: add setting to hide builtin masks 2023-07-05 00:07:26 +07:00
Yifei Zhang
38f2495cf6 Merge pull request #2253 from Yidadaa/bugfix-0704
feat: close #2192 use /list/models to get model ids
2023-07-04 23:21:32 +08:00
Yidadaa
4131fccbe0 feat: close #2192 use /list/models to get model ids 2023-07-04 23:16:24 +08:00
Yidadaa
f2d748cfe4 refactor: improve modal ux 2023-07-04 22:08:41 +08:00
Yifei Zhang
197ec0c29c Merge pull request #2242 from PaRaD1SE98/main
fix: temperature -> top_p
2023-07-04 11:28:43 +08:00
PaRaD1SE98
0a2af9335c fix: temperature -> top_p 2023-07-04 02:49:05 +09:00
Yifei Zhang
a52fa28ed1 Update tauri.conf.json 2023-07-04 01:10:16 +08:00
Yifei Zhang
78ed24dbf6 Merge pull request #2241 from Yidadaa/bugfix-0704
feat: add top p config
2023-07-04 00:42:41 +08:00
Yidadaa
cda074fe24 fixup 2023-07-04 00:40:57 +08:00
Yidadaa
823032617d feat: add top p config 2023-07-04 00:39:54 +08:00
Yidadaa
5963459499 fix: #2221 user prompts in front of all prompts 2023-07-04 00:22:30 +08:00
Yidadaa
0bc2c71b0c fix: #2230 hide chat actions for context prompts 2023-07-04 00:14:57 +08:00
Yifei Zhang
b4e350e189 Merge pull request #2234 from Yidadaa/dependabot/npm_and_yarn/prettier-2.8.8
chore(deps-dev): bump prettier from 2.8.7 to 2.8.8
2023-07-03 23:59:23 +08:00
Yifei Zhang
941e46490f Merge pull request #2235 from Yidadaa/dependabot/npm_and_yarn/rehype-katex-6.0.3
chore(deps): bump rehype-katex from 6.0.2 to 6.0.3
2023-07-03 23:59:12 +08:00
Yifei Zhang
4df92e903a Merge pull request #2236 from Yidadaa/dependabot/npm_and_yarn/hello-pangea/dnd-16.3.0
chore(deps): bump @hello-pangea/dnd from 16.2.0 to 16.3.0
2023-07-03 23:59:02 +08:00
Yifei Zhang
6ba02d0d50 Merge pull request #2237 from Yidadaa/dependabot/npm_and_yarn/types/node-20.3.3
chore(deps-dev): bump @types/node from 20.3.1 to 20.3.3
2023-07-03 23:58:47 +08:00
dependabot[bot]
2dc122831b chore(deps-dev): bump @types/node from 20.3.1 to 20.3.3
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 20.3.1 to 20.3.3.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-03 10:27:32 +00:00
dependabot[bot]
f3f84e523a chore(deps): bump @hello-pangea/dnd from 16.2.0 to 16.3.0
Bumps [@hello-pangea/dnd](https://github.com/hello-pangea/dnd) from 16.2.0 to 16.3.0.
- [Release notes](https://github.com/hello-pangea/dnd/releases)
- [Changelog](https://github.com/hello-pangea/dnd/blob/main/CHANGELOG.md)
- [Commits](https://github.com/hello-pangea/dnd/compare/v16.2.0...v16.3.0)

---
updated-dependencies:
- dependency-name: "@hello-pangea/dnd"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-03 10:27:22 +00:00
dependabot[bot]
0cdee25b5b chore(deps): bump rehype-katex from 6.0.2 to 6.0.3
Bumps [rehype-katex](https://github.com/remarkjs/remark-math) from 6.0.2 to 6.0.3.
- [Release notes](https://github.com/remarkjs/remark-math/releases)
- [Commits](https://github.com/remarkjs/remark-math/compare/rehype-katex@6.0.2...rehype-katex@6.0.3)

---
updated-dependencies:
- dependency-name: rehype-katex
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-03 10:27:11 +00:00
dependabot[bot]
92b0314c14 chore(deps-dev): bump prettier from 2.8.7 to 2.8.8
Bumps [prettier](https://github.com/prettier/prettier) from 2.8.7 to 2.8.8.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/2.8.7...2.8.8)

---
updated-dependencies:
- dependency-name: prettier
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-03 10:27:02 +00:00
Yidadaa
ad2bc7da96 Revert "feat: build universal app darwin bundle"
This reverts commit d8b606dc83.
2023-07-01 19:05:36 +08:00
Yidadaa
d8b606dc83 feat: build universal app darwin bundle 2023-07-01 18:54:12 +08:00
Yidadaa
5ce53dbcf4 fix: action ubuntu version 2023-07-01 18:39:48 +08:00
Yifei Zhang
c5c1a9ab3c Merge pull request #2209 from tdzz1102/main
Update Japanese translation
2023-07-01 18:32:12 +08:00
Yifei Zhang
564709aa98 Merge pull request #2213 from Yidadaa/bugfix-0701
feat: mac os arm64 bundle
2023-07-01 18:31:56 +08:00
Yidadaa
475158a145 feat: close #2194 add macos arm support 2023-07-01 18:28:47 +08:00
Yidadaa
829df56733 fix: #2195 correct macos icon size 2023-07-01 18:22:17 +08:00
Yidadaa
ee55f8790e fix: #2208 use global settings button dose not work 2023-07-01 18:16:06 +08:00
tdzz1102
6d19fb3909 update japanese translate 2023-07-01 12:28:08 +08:00
Yifei Zhang
9057712c8f Update chat.module.scss 2023-06-30 16:08:45 +08:00
Yifei Zhang
5d6e7de667 Merge pull request #2198 from YBY2020/main
perf : 代码细节优化
2023-06-30 14:35:41 +08:00
兀米
8f6f70879c Merge pull request #1 from YBY2020/YBY2020-patch-1
perf : 代码细节优化
2023-06-30 13:30:13 +08:00
兀米
3120087992 perf : 代码细节优化
大神, 学习代码过程中发现的一个小小的点,887行已定义`const isUser = message.role === "user";`,此处可直接用isUser
2023-06-30 12:00:03 +08:00
Yidadaa
0ec4cc223f fixup 2023-06-30 02:35:40 +08:00
Yidadaa
60c7be31b6 fixup 2023-06-30 02:05:17 +08:00
Yifei Zhang
2388f853c9 Merge pull request #2193 from Yidadaa/bugfix-0629
feat: improve ux & auto updater
2023-06-30 01:17:44 +08:00
Yidadaa
1e8d4763bb fix: tauri auto updater url 2023-06-30 01:15:01 +08:00
Yidadaa
be4834688d feat: close #2190 improve app auto updater 2023-06-30 00:39:54 +08:00
Yidadaa
3937dad6a6 feat: close #2187 improve chat actions ux 2023-06-30 00:39:54 +08:00
Yidadaa
463251dcc1 feat: allow to disable chunk building by setting DISABLE_CHUNK=1 2023-06-30 00:39:19 +08:00
Yifei Zhang
3adca26808 Update chat.module.scss 2023-06-29 11:03:13 +08:00
Yifei Zhang
97a8bb52d6 Update ui-lib.module.scss 2023-06-29 01:40:54 +08:00
Yifei Zhang
7748a980f9 Merge pull request #2185 from Yidadaa/bugfix-0628
feat: many ux improvments
2023-06-29 01:32:57 +08:00
Yidadaa
b044e274aa feat: close #2136 click avatar to edit message 2023-06-29 01:31:27 +08:00
Yidadaa
6c3d4a11cc feat: close #2141 danger zone 2023-06-29 01:09:51 +08:00
Yidadaa
98afd5516b feat: close #1994 add clipboard write api 2023-06-29 00:27:18 +08:00
Yidadaa
ea6926cad3 feat: replace window.prompt with showPrompt 2023-06-29 00:09:56 +08:00
Yidadaa
3298961748 feat: replace window.confirm with showConfirm 2023-06-28 23:40:39 +08:00
Yidadaa
0140f771c6 feat: close #2175 use default api host if endpoint is empty 2023-06-28 23:12:35 +08:00
Yidadaa
64c4f512ce fixup: #2149 chat action button style 2023-06-28 01:15:55 +08:00
Yidadaa
5b1d45c1a9 fix: #2149 try to fix chat action button style 2023-06-28 01:09:25 +08:00
Yifei Zhang
efbd1c15a9 Merge pull request #2174 from Yidadaa/bugfix-0627
fix: #2149 try to fix chat action button style
2023-06-28 01:03:19 +08:00
Yidadaa
bce74890dc fix: #2149 try to fix chat action button style 2023-06-28 01:02:56 +08:00
Yifei Zhang
eb93a43d13 Merge pull request #2173 from Yidadaa/bugfix-0627
feat: improve chat commands
2023-06-28 00:46:38 +08:00
Yidadaa
1dd75b63de feat: [WIP] support webdav 2023-06-28 00:34:01 +08:00
Yidadaa
6caf79121b feat: improve chat commands 2023-06-27 23:39:27 +08:00
Yifei Zhang
5cdd34a388 Merge pull request #2171 from Yidadaa/bugfix-0627
fix: #2149 try to fix ChatAction style on ios mobile
2023-06-27 23:31:04 +08:00
Yidadaa
38c8ee8cd2 feat: improve message item buttons style 2023-06-27 23:29:53 +08:00
Yidadaa
d5c33a1183 fix: #2149 try to fix ChatAction style on ios mobile 2023-06-27 23:07:12 +08:00
Yifei Zhang
058e28911a fix: #2135 disable cmd + arrow to change session 2023-06-26 18:29:46 +08:00
Yifei Zhang
25a3bb351d Merge pull request #2143 from yorunning/fix2
fix: alignment of mobile settings page title.
2023-06-26 13:27:00 +08:00
Yifei Zhang
1607640bed Merge pull request #2145 from Yidadaa/bugfix-0626
feat: #2144 improve input template
2023-06-26 13:21:48 +08:00
Yidadaa
5f0cda829f feat: #2144 improve input template 2023-06-26 13:18:59 +08:00
Yorun
b003a374b8 fix: alignment of mobile settings page title. 2023-06-26 04:16:06 +00:00
Yifei Zhang
5f7c262759 Merge pull request #2134 from Yidadaa/bugfix-0625
fix: input-range style
2023-06-26 01:51:55 +08:00
Yidadaa
f3ab6b27c9 fix: input-range style 2023-06-26 01:51:35 +08:00
Yifei Zhang
d62eb56886 Merge pull request #2133 from quzard/fix_pin_bug
Fix Pin Message
2023-06-26 01:43:20 +08:00
quzard
c2724c6aad Fix Pin Message: when userMessageIndex === 0, pinning a message was not possible. 2023-06-26 01:36:27 +08:00
Yifei Zhang
0cde1683b6 Update window.scss 2023-06-26 01:35:38 +08:00
Yifei Zhang
128f596f93 Update tauri.conf.json 2023-06-26 01:31:01 +08:00
Yifei Zhang
27fae8eed0 Merge pull request #2132 from Yidadaa/bugfix-0625
feat: align chat page title center on mobile screen
2023-06-26 01:28:56 +08:00
Yidadaa
82ec4474c2 feat: align chat page title center on mobile screen 2023-06-26 01:27:36 +08:00
Yifei Zhang
0d85dae239 Merge pull request #2131 from Yidadaa/bugfix-0625
feat: close #1615 pin messages to contextual prompts
2023-06-26 00:40:07 +08:00
Yidadaa
7893693706 feat: close #1615 pin messages to contextual prompts 2023-06-26 00:21:58 +08:00
Yidadaa
25ce6af36e feat: new chat message actions style 2023-06-25 23:53:22 +08:00
Yifei Zhang
c0269254e8 Merge pull request #2129 from Constaline/main
feat: add "Hide_Balance_Query" environment variable
2023-06-25 22:01:27 +08:00
Constaline
2f2aefd48e fix: typo 2023-06-25 21:33:22 +08:00
Constaline
c05de45d99 feat: add "Hide_Balance_Query" environment variable 2023-06-25 20:20:36 +08:00
Yifei Zhang
8f66da1128 Merge pull request #2119 from Yidadaa/bugfix-0624
chore: delete chat button position
2023-06-24 23:54:14 +08:00
Yidadaa
b5eaa8272b chore: delete chat button position 2023-06-24 23:53:37 +08:00
Yifei Zhang
7ee062e1de Merge pull request #2118 from Yidadaa/bugfix-0624
feat: #2013 #628 switch model button & chat commands
2023-06-24 23:40:54 +08:00
Yidadaa
8915af9b6d fixup 2023-06-24 23:39:38 +08:00
Yidadaa
ae1ef3215b feat: close #628 add chat commands 2023-06-24 23:38:11 +08:00
Yidadaa
5d06fa217c feat: close #2013 add switch model button to chat actions 2023-06-24 22:41:13 +08:00
Yidadaa
50b1f7db12 feat: right-click to copy message to user input 2023-06-24 22:32:46 +08:00
Yifei Zhang
fb82806956 Merge pull request #2117 from JunJD/优化useScrollToBottom
refactor: 优化 useScrollToBottom
2023-06-24 22:25:45 +08:00
Yifei Zhang
0658a93962 Merge pull request #2116 from Zizwar/main
Added support for Arabic language and right-to-left direction
2023-06-24 18:37:52 +08:00
Yifei Zhang
f33be37070 Update constant.ts 2023-06-24 16:50:05 +08:00
dingjunjie
9278d7f851 refactor: 优化 useScrollToBottom 2023-06-24 16:37:29 +08:00
Yifei Zhang
35ba898c97 Merge pull request #2112 from songxiaocheng/PR/history-message-count
fix: #2111 make "Attached Messages Count" working
2023-06-24 14:34:48 +08:00
Zizwar
15f8d13d81 Set markdown-body RTL for Arabic text 2023-06-24 05:53:58 +00:00
Zizwar
946e508e8f Support left to right in language arabic 2023-06-24 05:10:34 +00:00
Zizwar
35b4125b98 Added support Arabic language 2023-06-24 04:30:52 +00:00
Xiaocheng Song
91d8f9d73e fix: #2111 make "Attached Messages Count" working 2023-06-24 05:53:39 +08:00
Yifei Zhang
c636993989 Merge pull request #2110 from Yidadaa/bugfix-0623
doc: #695 how to use a proxy with password in docker
2023-06-24 00:30:17 +08:00
Yidadaa
65a24c16bc doc: #695 how to use a proxy with password in docker 2023-06-24 00:29:47 +08:00
Yifei Zhang
15ce114440 Merge pull request #2109 from Yidadaa/bugfix-0623
feat: close #1789 add user input template
2023-06-24 00:20:05 +08:00
Yidadaa
1722f75dcb fixup 2023-06-24 00:18:27 +08:00
Yidadaa
be597a551d feat: #close 1789 add user input template 2023-06-24 00:07:46 +08:00
Yifei Zhang
56bc945335 Merge pull request #2107 from Yidadaa/bugfix-0623
fix: #2087 chat box font size use global config
2023-06-23 20:58:09 +08:00
Yidadaa
fa9ceb5875 fix: #2087 chat box font size use global config 2023-06-23 20:57:43 +08:00
Yifei Zhang
0ccf5a16c7 Merge pull request #2106 from Yidadaa/bugfix-0623
feat: close #2025 just use a smaller to-bottom threshold
2023-06-23 20:53:28 +08:00
Yidadaa
93c666b03d feat: close #2025 just use a smaller to-bottom threshold 2023-06-23 20:53:08 +08:00
Yifei Zhang
d5c28b506c Update README.md 2023-06-23 20:21:41 +08:00
Yifei Zhang
3318f01aa1 Merge pull request #2099 from johan456789/patch-1
add explanation and link to FAQ
2023-06-23 13:20:02 +08:00
Tsung-Han Yu
1641019091 add explanation and link to FAQ 2023-06-23 05:54:39 +08:00
Yifei Zhang
3572969866 Merge pull request #2096 from Yidadaa/dependabot/cargo/src-tauri/openssl-0.10.55
chore(deps): bump openssl from 0.10.52 to 0.10.55 in /src-tauri
2023-06-22 17:43:41 +08:00
Yifei Zhang
ad1fe8762b Merge pull request #2092 from PaRaD1SE98/main
fix: migrate modelConfig state
2023-06-22 11:31:46 +08:00
dependabot[bot]
c9c3e32a78 chore(deps): bump openssl from 0.10.52 to 0.10.55 in /src-tauri
Bumps [openssl](https://github.com/sfackler/rust-openssl) from 0.10.52 to 0.10.55.
- [Release notes](https://github.com/sfackler/rust-openssl/releases)
- [Commits](https://github.com/sfackler/rust-openssl/compare/openssl-v0.10.52...openssl-v0.10.55)

---
updated-dependencies:
- dependency-name: openssl
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-21 22:57:40 +00:00
PaRaD1SE98
f4c99c9cf7 fix: migrate modelConfig state 2023-06-22 02:11:07 +09:00
Yifei Zhang
57447c24c8 Update tauri.conf.json 2023-06-21 15:36:17 +08:00
Yifei Zhang
514ed03378 Merge pull request #2065 from macvip/window-state
restore last window state (position and size) on startup
2023-06-21 02:30:25 +08:00
Yifei Zhang
078b039cf1 Merge pull request #2064 from mushan0x0/patch-1
fix: #2061 RequestInit TypeError
2023-06-20 18:04:24 +08:00
macvip
a120670785 restore last window state (position and size) on startup 2023-06-20 13:06:54 +08:00
mushan0x0
9c05d136f5 fix: #2061 RequestInit TypeError 2023-06-20 11:57:31 +08:00
Yifei Zhang
daeb8a67a3 Merge pull request #2048 from ilizhu/main
feat: i18n in message exporter
2023-06-20 02:45:48 +08:00
Yifei Zhang
c64888cccd Merge pull request #2051 from Yidadaa/dependabot/npm_and_yarn/next-13.4.6
chore(deps): bump next from 13.4.3 to 13.4.6
2023-06-20 02:40:23 +08:00
Yifei Zhang
6fc2637870 Merge pull request #2050 from Yidadaa/dependabot/npm_and_yarn/zustand-4.3.8
chore(deps): bump zustand from 4.3.6 to 4.3.8
2023-06-20 02:40:14 +08:00
Yifei Zhang
fc4df23075 Merge pull request #2053 from Yidadaa/dependabot/npm_and_yarn/lint-staged-13.2.2
chore(deps-dev): bump lint-staged from 13.2.0 to 13.2.2
2023-06-20 02:40:00 +08:00
Yifei Zhang
3ae502986b Merge pull request #2054 from Yidadaa/dependabot/npm_and_yarn/types/react-18.2.12
chore(deps-dev): bump @types/react from 18.0.31 to 18.2.12
2023-06-20 02:39:50 +08:00
Yifei Zhang
67f23d202a Merge pull request #2056 from Yidadaa/bugfix-0620
fix: #2055 should render mermaid completely
2023-06-20 02:38:59 +08:00
Yidadaa
3c38b9c93b fix: #2055 should render mermaid completely 2023-06-20 02:37:38 +08:00
dependabot[bot]
152a00baf5 chore(deps-dev): bump @types/react from 18.0.31 to 18.2.12
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 18.0.31 to 18.2.12.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-19 11:10:39 +00:00
dependabot[bot]
405d02023c chore(deps-dev): bump lint-staged from 13.2.0 to 13.2.2
Bumps [lint-staged](https://github.com/okonet/lint-staged) from 13.2.0 to 13.2.2.
- [Release notes](https://github.com/okonet/lint-staged/releases)
- [Commits](https://github.com/okonet/lint-staged/compare/v13.2.0...v13.2.2)

---
updated-dependencies:
- dependency-name: lint-staged
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-19 11:10:15 +00:00
dependabot[bot]
4ba2d0d7e0 chore(deps): bump next from 13.4.3 to 13.4.6
Bumps [next](https://github.com/vercel/next.js) from 13.4.3 to 13.4.6.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v13.4.3...v13.4.6)

---
updated-dependencies:
- dependency-name: next
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-19 11:09:00 +00:00
dependabot[bot]
38ce230cf5 chore(deps): bump zustand from 4.3.6 to 4.3.8
Bumps [zustand](https://github.com/pmndrs/zustand) from 4.3.6 to 4.3.8.
- [Release notes](https://github.com/pmndrs/zustand/releases)
- [Commits](https://github.com/pmndrs/zustand/compare/v4.3.6...v4.3.8)

---
updated-dependencies:
- dependency-name: zustand
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-19 11:06:59 +00:00
zhuge
c6696313bf 聊天记录导出格式为image时预览聊天信息项使用设置的语言 chat information items using the language in the settings when exporting chat records in image format 2023-06-19 16:16:09 +08:00
Yifei Zhang
89717c9b15 Update issue-translator.yml 2023-06-18 18:28:31 +08:00
Yifei Zhang
3aca699ad9 Create issue-translator.yml 2023-06-18 18:27:18 +08:00
Yifei Zhang
f3ec9f02eb Merge pull request #1807 from AngelLiang/feat/frequency_penalty
feat: Add frequency_penalty request parameter
2023-06-18 14:36:55 +08:00
Yifei Zhang
1c7f3e6057 Merge pull request #2028 from Yidadaa/bugfix-0618
fix: #1771 should not lose chat context when sumindex > n - count
2023-06-18 00:52:41 +08:00
Yidadaa
db63a5a670 fix: #1771 should not lose chat context when sumindex > n - count 2023-06-18 00:51:16 +08:00
Yifei Zhang
0a2cccfd6e Merge pull request #2018 from Yidadaa/bugfix-0617
feat: add nynorsk option
2023-06-17 01:20:06 +08:00
Yidadaa
113bd24796 feat: add nynorsk option 2023-06-17 01:03:06 +08:00
Yifei Zhang
374a27a72e Merge pull request #1065 from perandre/norwegian-translation
Add Norwegian translation
2023-06-17 00:33:26 +08:00
Xavier Barbosa
567eff9ef4 Remove deprecated keys 2023-06-16 16:00:57 +01:00
Xavier Barbosa
bfe33af03d Merge branch 'main' into norwegian-translation 2023-06-16 16:00:17 +01:00
Yifei Zhang
7ebc5eb70f Merge pull request #2002 from yeenbean/CVE-2023-2251
Remediation of CVE-2023-2251
2023-06-16 18:11:09 +08:00
Yifei Zhang
52287fd01d Merge pull request #2009 from taro-ishihara/i18n/japanese-translation
Fix Japanese language file
2023-06-16 18:10:56 +08:00
Yifei Zhang
e0f2bc2725 Merge pull request #2005 from Yidadaa/dependabot/npm_and_yarn/use-debounce-9.0.4
chore(deps): bump use-debounce from 9.0.3 to 9.0.4
2023-06-16 16:51:48 +08:00
Yifei Zhang
4355ec3b44 Merge pull request #2007 from Yidadaa/dependabot/npm_and_yarn/types/node-20.3.1
chore(deps-dev): bump @types/node from 18.15.11 to 20.3.1
2023-06-16 16:51:21 +08:00
Yifei Zhang
0c2f11ffd7 Merge pull request #2008 from Yidadaa/dependabot/npm_and_yarn/react-markdown-8.0.7
chore(deps): bump react-markdown from 8.0.6 to 8.0.7
2023-06-16 16:51:10 +08:00
Yifei Zhang
65e2f38fac Merge pull request #2004 from Yidadaa/dependabot/npm_and_yarn/mermaid-10.2.3
chore(deps): bump mermaid from 10.1.0 to 10.2.3
2023-06-16 16:50:51 +08:00
Taro Ishihara
aeaefbf8a9 Fix Japanese language file 2023-06-16 00:23:56 -07:00
dependabot[bot]
719cb3ae05 chore(deps): bump react-markdown from 8.0.6 to 8.0.7
Bumps [react-markdown](https://github.com/remarkjs/react-markdown) from 8.0.6 to 8.0.7.
- [Release notes](https://github.com/remarkjs/react-markdown/releases)
- [Changelog](https://github.com/remarkjs/react-markdown/blob/main/changelog.md)
- [Commits](https://github.com/remarkjs/react-markdown/compare/8.0.6...8.0.7)

---
updated-dependencies:
- dependency-name: react-markdown
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-16 07:22:32 +00:00
dependabot[bot]
08816c8f7d chore(deps-dev): bump @types/node from 18.15.11 to 20.3.1
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 18.15.11 to 20.3.1.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-16 07:22:15 +00:00
dependabot[bot]
1031203e3f chore(deps): bump use-debounce from 9.0.3 to 9.0.4
Bumps [use-debounce](https://github.com/xnimorz/use-debounce) from 9.0.3 to 9.0.4.
- [Release notes](https://github.com/xnimorz/use-debounce/releases)
- [Changelog](https://github.com/xnimorz/use-debounce/blob/master/CHANGELOG.md)
- [Commits](https://github.com/xnimorz/use-debounce/compare/9.0.3...9.0.4)

---
updated-dependencies:
- dependency-name: use-debounce
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-16 07:21:24 +00:00
dependabot[bot]
63ef6e7d3d chore(deps): bump mermaid from 10.1.0 to 10.2.3
Bumps [mermaid](https://github.com/mermaid-js/mermaid) from 10.1.0 to 10.2.3.
- [Release notes](https://github.com/mermaid-js/mermaid/releases)
- [Changelog](https://github.com/mermaid-js/mermaid/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/mermaid-js/mermaid/compare/v10.1.0...v10.2.3)

---
updated-dependencies:
- dependency-name: mermaid
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-16 07:20:57 +00:00
Yifei Zhang
590c9bb978 Create dependabot.yml 2023-06-16 15:12:47 +08:00
Joshua
5055a8c7a3 yarn.lock also required for CVE-2023-2251 🙃 2023-06-15 23:54:53 -07:00
Joshua
284c3f27e7 Resolves found vuln CVE-2023-2251 2023-06-15 23:33:08 -07:00
Yifei Zhang
468f198a6b Update README.md 2023-06-16 01:42:51 +08:00
Yifei Zhang
b0c6f6d4d3 Update tauri.conf.json 2023-06-16 00:53:10 +08:00
Yidadaa
473061b4d5 fixup: add TAURI_KEY_PASSWORD secrets 2023-06-16 00:18:54 +08:00
AngelLiang
72cbb156ae feat: Add frequency_penalty request parameter 2023-06-04 16:05:35 +08:00
Per André Rønsen
5a10b2a75f Update no.ts with const no: LocaleType 2023-05-10 14:59:57 +01:00
Per André Rønsen
72b553efe8 Add files for Norwegian translation 2023-05-10 14:59:36 +01:00
349 changed files with 25592 additions and 3876 deletions

14
.babelrc Normal file
View File

@@ -0,0 +1,14 @@
{
"presets": [
[
"next/babel",
{
"preset-env": {
"targets": {
"browsers": ["> 0.25%, not dead"]
}
}
}
]
]
}

97
.dockerignore Normal file
View File

@@ -0,0 +1,97 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Node.js dependencies
/node_modules
/jspm_packages
# TypeScript v1 declaration files
typings
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.test
# local env files
.env*.local
# Next.js build output
.next
out
# Nuxt.js build output
.nuxt
dist
# Gatsby files
.cache/
# Vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# Temporary folders
tmp
temp
# IDE and editor directories
.idea
.vscode
*.swp
*.swo
*~
# OS generated files
.DS_Store
Thumbs.db
# secret key
*.key
*.key.pub

View File

@@ -2,12 +2,22 @@
# Your openai api key. (required)
OPENAI_API_KEY=sk-xxxx
# Access passsword, separated by comma. (optional)
# Access password, separated by comma. (optional)
CODE=your-password
# You can start service behind a proxy
PROXY_URL=http://localhost:7890
# (optional)
# Default: Empty
# Googel Gemini Pro API key, set if you want to use Google Gemini Pro API.
GOOGLE_API_KEY=
# (optional)
# Default: https://generativelanguage.googleapis.com/
# Googel Gemini Pro API url without pathname, set if you want to customize Google Gemini Pro API url.
GOOGLE_URL=
# Override openai api request base url. (optional)
# Default: https://api.openai.com
# Examples: http://your-openai-proxy.com
@@ -15,9 +25,13 @@ BASE_URL=
# Specify OpenAI organization ID.(optional)
# Default: Empty
# If you do not want users to input their own API key, set this value to 1.
OPENAI_ORG_ID=
# (optional)
# Default: Empty
# If you do not want users to use GPT-4, set this value to 1.
DISABLE_GPT4=
# (optional)
# Default: Empty
# If you do not want users to input their own API key, set this value to 1.
@@ -25,5 +39,25 @@ HIDE_USER_API_KEY=
# (optional)
# Default: Empty
# If you do not want users to use GPT-4, set this value to 1.
DISABLE_GPT4=
# If you do want users to query balance, set this value to 1.
ENABLE_BALANCE_QUERY=
# (optional)
# Default: Empty
# If you want to disable parse settings from url, set this value to 1.
DISABLE_FAST_LINK=
# anthropic claude Api Key.(optional)
ANTHROPIC_API_KEY=
### anthropic claude Api version. (optional)
ANTHROPIC_API_VERSION=
### anthropic claude Api url (optional)
ANTHROPIC_URL=
### (optional)
WHITE_WEBDEV_ENDPOINTS=

View File

@@ -1,4 +1,12 @@
{
"extends": "next/core-web-vitals",
"plugins": ["prettier"]
"plugins": [
"prettier"
],
"parserOptions": {
"ecmaFeatures": {
"legacyDecorators": true
}
},
"ignorePatterns": ["globals.css"]
}

View File

@@ -1,43 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: "[Bug] "
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Deployment**
- [ ] Docker
- [ ] Vercel
- [ ] Server
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional Logs**
Add any logs about the problem here.

146
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,146 @@
name: Bug report
description: Create a report to help us improve
title: "[Bug] "
labels: ["bug"]
body:
- type: markdown
attributes:
value: "## Describe the bug"
- type: textarea
id: bug-description
attributes:
label: "Bug Description"
description: "A clear and concise description of what the bug is."
placeholder: "Explain the bug..."
validations:
required: true
- type: markdown
attributes:
value: "## To Reproduce"
- type: textarea
id: steps-to-reproduce
attributes:
label: "Steps to Reproduce"
description: "Steps to reproduce the behavior:"
placeholder: |
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
validations:
required: true
- type: markdown
attributes:
value: "## Expected behavior"
- type: textarea
id: expected-behavior
attributes:
label: "Expected Behavior"
description: "A clear and concise description of what you expected to happen."
placeholder: "Describe what you expected to happen..."
validations:
required: true
- type: markdown
attributes:
value: "## Screenshots"
- type: textarea
id: screenshots
attributes:
label: "Screenshots"
description: "If applicable, add screenshots to help explain your problem."
placeholder: "Paste your screenshots here or write 'N/A' if not applicable..."
validations:
required: false
- type: markdown
attributes:
value: "## Deployment"
- type: checkboxes
id: deployment
attributes:
label: "Deployment Method"
description: "Please select the deployment method you are using."
options:
- label: "Docker"
- label: "Vercel"
- label: "Server"
- type: markdown
attributes:
value: "## Desktop (please complete the following information):"
- type: input
id: desktop-os
attributes:
label: "Desktop OS"
description: "Your desktop operating system."
placeholder: "e.g., Windows 10"
validations:
required: false
- type: input
id: desktop-browser
attributes:
label: "Desktop Browser"
description: "Your desktop browser."
placeholder: "e.g., Chrome, Safari"
validations:
required: false
- type: input
id: desktop-version
attributes:
label: "Desktop Browser Version"
description: "Version of your desktop browser."
placeholder: "e.g., 89.0"
validations:
required: false
- type: markdown
attributes:
value: "## Smartphone (please complete the following information):"
- type: input
id: smartphone-device
attributes:
label: "Smartphone Device"
description: "Your smartphone device."
placeholder: "e.g., iPhone X"
validations:
required: false
- type: input
id: smartphone-os
attributes:
label: "Smartphone OS"
description: "Your smartphone operating system."
placeholder: "e.g., iOS 14.4"
validations:
required: false
- type: input
id: smartphone-browser
attributes:
label: "Smartphone Browser"
description: "Your smartphone browser."
placeholder: "e.g., Safari"
validations:
required: false
- type: input
id: smartphone-version
attributes:
label: "Smartphone Browser Version"
description: "Version of your smartphone browser."
placeholder: "e.g., 14"
validations:
required: false
- type: markdown
attributes:
value: "## Additional Logs"
- type: textarea
id: additional-logs
attributes:
label: "Additional Logs"
description: "Add any logs about the problem here."
placeholder: "Paste any relevant logs here..."
validations:
required: false

View File

@@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[Feature] "
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -0,0 +1,53 @@
name: Feature request
description: Suggest an idea for this project
title: "[Feature Request]: "
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: "## Is your feature request related to a problem? Please describe."
- type: textarea
id: problem-description
attributes:
label: Problem Description
description: "A clear and concise description of what the problem is. Example: I'm always frustrated when [...]"
placeholder: "Explain the problem you are facing..."
validations:
required: true
- type: markdown
attributes:
value: "## Describe the solution you'd like"
- type: textarea
id: desired-solution
attributes:
label: Solution Description
description: A clear and concise description of what you want to happen.
placeholder: "Describe the solution you'd like..."
validations:
required: true
- type: markdown
attributes:
value: "## Describe alternatives you've considered"
- type: textarea
id: alternatives-considered
attributes:
label: Alternatives Considered
description: A clear and concise description of any alternative solutions or features you've considered.
placeholder: "Describe any alternative solutions or features you've considered..."
validations:
required: false
- type: markdown
attributes:
value: "## Additional context"
- type: textarea
id: additional-context
attributes:
label: Additional Context
description: Add any other context or screenshots about the feature request here.
placeholder: "Add any other context or screenshots about the feature request here..."
validations:
required: false

View File

@@ -1,24 +0,0 @@
---
name: 功能建议
about: 请告诉我们你的灵光一闪
title: "[Feature] "
labels: ''
assignees: ''
---
> 为了提高交流效率,我们设立了官方 QQ 群和 QQ 频道,如果你在使用或者搭建过程中遇到了任何问题,请先第一时间加群或者频道咨询解决,除非是可以稳定复现的 Bug 或者较为有创意的功能建议,否则请不要随意往 Issue 区发送低质无意义帖子。
> [点击加入官方群聊](https://github.com/Yidadaa/ChatGPT-Next-Web/discussions/1724)
**这个功能与现有的问题有关吗?**
如果有关,请在此列出链接或者描述问题。
**你想要什么功能或者有什么建议?**
尽管告诉我们。
**有没有可以参考的同类竞品?**
可以给出参考产品的链接或者截图。
**其他信息**
可以说说你的其他考虑。

View File

@@ -1,36 +0,0 @@
---
name: 反馈问题
about: 请告诉我们你遇到的问题
title: "[Bug] "
labels: ''
assignees: ''
---
> 为了提高交流效率,我们设立了官方 QQ 群和 QQ 频道,如果你在使用或者搭建过程中遇到了任何问题,请先第一时间加群或者频道咨询解决,除非是可以稳定复现的 Bug 或者较为有创意的功能建议,否则请不要随意往 Issue 区发送低质无意义帖子。
> [点击加入官方群聊](https://github.com/Yidadaa/ChatGPT-Next-Web/discussions/1724)
**反馈须知**
⚠️ 注意:不遵循此模板的任何帖子都会被立即关闭,如果没有提供下方的信息,我们无法定位你的问题。
请在下方中括号内输入 x 来表示你已经知晓相关内容。
- [ ] 我确认已经在 [常见问题](https://github.com/Yidadaa/ChatGPT-Next-Web/blob/main/docs/faq-cn.md) 中搜索了此次反馈的问题,没有找到解答;
- [ ] 我确认已经在 [Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) 列表(包括已经 Close 的)中搜索了此次反馈的问题,没有找到解答。
- [ ] 我确认已经在 [Vercel 使用教程](https://github.com/Yidadaa/ChatGPT-Next-Web/blob/main/docs/vercel-cn.md) 中搜索了此次反馈的问题,没有找到解答。
**描述问题**
请在此描述你遇到了什么问题。
**如何复现**
请告诉我们你是通过什么操作触发的该问题。
**截图**
请在此提供控制台截图、屏幕截图或者服务端的 log 截图。
**一些必要的信息**
- 系统:[比如 windows 10/ macos 12/ linux / android 11 / ios 16]
- 浏览器: [比如 chrome, safari]
- 版本: [填写设置页面的版本号]
- 部署方式:[比如 vercel、docker 或者服务器部署]

11
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,11 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "npm" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"

View File

@@ -9,7 +9,7 @@ jobs:
create-release:
permissions:
contents: write
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
outputs:
release_id: ${{ steps.create-release.outputs.result }}
@@ -18,7 +18,7 @@ jobs:
- name: setup node
uses: actions/setup-node@v3
with:
node-version: 16
node-version: 18
- name: get version
run: echo "PACKAGE_VERSION=$(node -p "require('./src-tauri/tauri.conf.json').package.version")" >> $GITHUB_ENV
- name: create release
@@ -39,19 +39,34 @@ jobs:
strategy:
fail-fast: false
matrix:
platform: [macos-latest, ubuntu-20.04, windows-latest]
config:
- os: ubuntu-latest
arch: x86_64
rust_target: x86_64-unknown-linux-gnu
- os: macos-latest
arch: aarch64
rust_target: x86_64-apple-darwin,aarch64-apple-darwin
- os: windows-latest
arch: x86_64
rust_target: x86_64-pc-windows-msvc
runs-on: ${{ matrix.platform }}
runs-on: ${{ matrix.config.os }}
steps:
- uses: actions/checkout@v3
- name: setup node
uses: actions/setup-node@v3
with:
node-version: 16
node-version: 18
cache: 'yarn'
- name: install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.config.rust_target }}
- uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.config.os }}
- name: install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-20.04'
if: matrix.config.os == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
@@ -61,13 +76,21 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
with:
releaseId: ${{ needs.create-release.outputs.release_id }}
args: ${{ matrix.config.os == 'macos-latest' && '--target universal-apple-darwin' || '' }}
publish-release:
permissions:
contents: write
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
needs: [create-release, build-tauri]
steps:

84
.github/workflows/deploy_preview.yml vendored Normal file
View File

@@ -0,0 +1,84 @@
name: VercelPreviewDeployment
on:
pull_request_target:
types:
- opened
- synchronize
- reopened
env:
VERCEL_TEAM: ${{ secrets.VERCEL_TEAM }}
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
VERCEL_PR_DOMAIN_SUFFIX: ${{ secrets.VERCEL_PR_DOMAIN_SUFFIX }}
permissions:
contents: read
statuses: write
pull-requests: write
jobs:
deploy-preview:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Extract branch name
shell: bash
run: echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> "$GITHUB_OUTPUT"
id: extract_branch
- name: Hash branch name
uses: pplanel/hash-calculator-action@v1.3.1
id: hash_branch
with:
input: ${{ steps.extract_branch.outputs.branch }}
method: MD5
- name: Set Environment Variables
id: set_env
if: github.event_name == 'pull_request_target'
run: |
echo "VERCEL_ALIAS_DOMAIN=${{ github.event.pull_request.number }}-${{ github.workflow }}.${VERCEL_PR_DOMAIN_SUFFIX}" >> $GITHUB_OUTPUT
- name: Install Vercel CLI
run: npm install --global vercel@latest
- name: Cache dependencies
uses: actions/cache@v2
id: cache-npm
with:
path: ~/.npm
key: npm-${{ hashFiles('package-lock.json') }}
restore-keys: npm-
- name: Pull Vercel Environment Information
run: vercel pull --yes --environment=preview --token=${VERCEL_TOKEN}
- name: Deploy Project Artifacts to Vercel
id: vercel
env:
META_TAG: ${{ steps.hash_branch.outputs.digest }}-${{ github.run_number }}-${{ github.run_attempt}}
run: |
set -e
vercel pull --yes --environment=preview --token=${VERCEL_TOKEN}
vercel build --token=${VERCEL_TOKEN}
vercel deploy --prebuilt --archive=tgz --token=${VERCEL_TOKEN} --meta base_hash=${{ env.META_TAG }}
DEFAULT_URL=$(vercel ls --token=${VERCEL_TOKEN} --meta base_hash=${{ env.META_TAG }})
ALIAS_URL=$(vercel alias set ${DEFAULT_URL} ${{ steps.set_env.outputs.VERCEL_ALIAS_DOMAIN }} --token=${VERCEL_TOKEN} --scope ${VERCEL_TEAM}| awk '{print $3}')
echo "New preview URL: ${DEFAULT_URL}"
echo "New alias URL: ${ALIAS_URL}"
echo "VERCEL_URL=${ALIAS_URL}" >> "$GITHUB_OUTPUT"
- uses: mshick/add-pr-comment@v2
with:
message: |
Your build has completed!
[Preview deployment](${{ steps.vercel.outputs.VERCEL_URL }})

15
.github/workflows/issue-translator.yml vendored Normal file
View File

@@ -0,0 +1,15 @@
name: Issue Translator
on:
issue_comment:
types: [created]
issues:
types: [opened]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: usthe/issues-translate-action@v2.7
with:
IS_MODIFY_TITLE: false
CUSTOM_BOT_NOTE: Bot detected the issue body's language is not English, translate it automatically.

View File

@@ -0,0 +1,40 @@
name: Removedeploypreview
permissions:
contents: read
statuses: write
pull-requests: write
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
on:
pull_request_target:
types:
- closed
jobs:
delete-deployments:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Extract branch name
shell: bash
run: echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_OUTPUT
id: extract_branch
- name: Hash branch name
uses: pplanel/hash-calculator-action@v1.3.1
id: hash_branch
with:
input: ${{ steps.extract_branch.outputs.branch }}
method: MD5
- name: Call the delete-deployment-preview.sh script
env:
META_TAG: ${{ steps.hash_branch.outputs.digest }}
run: |
bash ./scripts/delete-deployment-preview.sh

View File

@@ -24,7 +24,7 @@ jobs:
id: sync
uses: aormsby/Fork-Sync-With-Upstream-action@v3.4
with:
upstream_sync_repo: Yidadaa/ChatGPT-Next-Web
upstream_sync_repo: ChatGPTNextWeb/ChatGPT-Next-Web
upstream_sync_branch: main
target_sync_branch: main
target_repo_token: ${{ secrets.GITHUB_TOKEN }} # automatically generated, no need to set

View File

@@ -16,6 +16,7 @@ FROM base AS builder
RUN apk update && apk add --no-cache git
ENV OPENAI_API_KEY=""
ENV GOOGLE_API_KEY=""
ENV CODE=""
WORKDIR /app
@@ -31,6 +32,7 @@ RUN apk add proxychains-ng
ENV PROXY_URL=""
ENV OPENAI_API_KEY=""
ENV GOOGLE_API_KEY=""
ENV CODE=""
COPY --from=builder /app/public ./public
@@ -41,22 +43,22 @@ COPY --from=builder /app/.next/server ./.next/server
EXPOSE 3000
CMD if [ -n "$PROXY_URL" ]; then \
export HOSTNAME="127.0.0.1"; \
protocol=$(echo $PROXY_URL | cut -d: -f1); \
host=$(echo $PROXY_URL | cut -d/ -f3 | cut -d: -f1); \
port=$(echo $PROXY_URL | cut -d: -f3); \
conf=/etc/proxychains.conf; \
echo "strict_chain" > $conf; \
echo "proxy_dns" >> $conf; \
echo "remote_dns_subnet 224" >> $conf; \
echo "tcp_read_time_out 15000" >> $conf; \
echo "tcp_connect_time_out 8000" >> $conf; \
echo "localnet 127.0.0.0/255.0.0.0" >> $conf; \
echo "localnet ::1/128" >> $conf; \
echo "[ProxyList]" >> $conf; \
echo "$protocol $host $port" >> $conf; \
cat /etc/proxychains.conf; \
proxychains -f $conf node server.js; \
export HOSTNAME="127.0.0.1"; \
protocol=$(echo $PROXY_URL | cut -d: -f1); \
host=$(echo $PROXY_URL | cut -d/ -f3 | cut -d: -f1); \
port=$(echo $PROXY_URL | cut -d: -f3); \
conf=/etc/proxychains.conf; \
echo "strict_chain" > $conf; \
echo "proxy_dns" >> $conf; \
echo "remote_dns_subnet 224" >> $conf; \
echo "tcp_read_time_out 15000" >> $conf; \
echo "tcp_connect_time_out 8000" >> $conf; \
echo "localnet 127.0.0.0/255.0.0.0" >> $conf; \
echo "localnet ::1/128" >> $conf; \
echo "[ProxyList]" >> $conf; \
echo "$protocol $host $port" >> $conf; \
cat /etc/proxychains.conf; \
proxychains -f $conf node server.js; \
else \
node server.js; \
node server.js; \
fi

88
LICENSE
View File

@@ -1,75 +1,21 @@
版权所有c<2023><Zhang Yifei>
MIT License
反996许可证版本1.0
Copyright (c) 2023 Zhang Yifei
在符合下列条件的情况下,
特此免费向任何得到本授权作品的副本(包括源代码、文件和/或相关内容,以下统称为“授权作品”
)的个人和法人实体授权:被授权个人或法人实体有权以任何目的处置授权作品,包括但不限于使
用、复制,修改,衍生利用、散布,发布和再许可:
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
1. 个人或法人实体必须在许可作品的每个再散布或衍生副本上包含以上版权声明和本许可证,不
得自行修改。
2. 个人或法人实体必须严格遵守与个人实际所在地或个人出生地或归化地、或法人实体注册地或
经营地(以较严格者为准)的司法管辖区所有适用的与劳动和就业相关法律、法规、规则和
标准。如果该司法管辖区没有此类法律、法规、规章和标准或其法律、法规、规章和标准不可
执行,则个人或法人实体必须遵守国际劳工标准的核心公约。
3. 个人或法人不得以任何方式诱导或强迫其全职或兼职员工或其独立承包人以口头或书面形式同
意直接或间接限制、削弱或放弃其所拥有的,受相关与劳动和就业有关的法律、法规、规则和
标准保护的权利或补救措施,无论该等书面或口头协议是否被该司法管辖区的法律所承认,该
等个人或法人实体也不得以任何方法限制其雇员或独立承包人向版权持有人或监督许可证合规
情况的有关当局报告或投诉上述违反许可证的行为的权利。
该授权作品是"按原样"提供,不做任何明示或暗示的保证,包括但不限于对适销性、特定用途适用
性和非侵权性的保证。在任何情况下,无论是在合同诉讼、侵权诉讼或其他诉讼中,版权持有人均
不承担因本软件或本软件的使用或其他交易而产生、引起或与之相关的任何索赔、损害或其他责任。
------------------------- ENGLISH ------------------------------
Copyright (c) <2023> <Zhang Yifei>
Anti 996 License Version 1.0 (Draft)
Permission is hereby granted to any individual or legal entity obtaining a copy
of this licensed work (including the source code, documentation and/or related
items, hereinafter collectively referred to as the "licensed work"), free of
charge, to deal with the licensed work for any purpose, including without
limitation, the rights to use, reproduce, modify, prepare derivative works of,
publish, distribute and sublicense the licensed work, subject to the following
conditions:
1. The individual or the legal entity must conspicuously display, without
modification, this License on each redistributed or derivative copy of the
Licensed Work.
2. The individual or the legal entity must strictly comply with all applicable
laws, regulations, rules and standards of the jurisdiction relating to
labor and employment where the individual is physically located or where
the individual was born or naturalized; or where the legal entity is
registered or is operating (whichever is stricter). In case that the
jurisdiction has no such laws, regulations, rules and standards or its
laws, regulations, rules and standards are unenforceable, the individual
or the legal entity are required to comply with Core International Labor
Standards.
3. The individual or the legal entity shall not induce or force its
employee(s), whether full-time or part-time, or its independent
contractor(s), in any methods, to agree in oral or written form,
to directly or indirectly restrict, weaken or relinquish his or
her rights or remedies under such laws, regulations, rules and
standards relating to labor and employment as mentioned above,
no matter whether such written or oral agreement are enforceable
under the laws of the said jurisdiction, nor shall such individual
or the legal entity limit, in any methods, the rights of its employee(s)
or independent contractor(s) from reporting or complaining to the copyright
holder or relevant authorities monitoring the compliance of the license
about its violation(s) of the said license.
THE LICENSED WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT
HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN ANY WAY CONNECTION
WITH THE LICENSED WORK OR THE USE OR OTHER DEALINGS IN THE LICENSED WORK.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

188
README.md
View File

@@ -1,19 +1,33 @@
<div align="center">
<img src="./docs/images/icon.svg" alt="icon"/>
<img src="./docs/images/head-cover.png" alt="icon"/>
<h1 align="center">ChatGPT Next Web</h1>
<h1 align="center">NextChat (ChatGPT Next Web)</h1>
English / [简体中文](./README_CN.md)
One-Click to deploy well-designed ChatGPT web UI on Vercel.
One-Click to get a well-designed cross-platform ChatGPT web UI, with GPT3, GPT4 & Gemini Pro support.
一键免费部署你的私人 ChatGPT 网页应用。
一键免费部署你的跨平台私人 ChatGPT 应用, 支持 GPT3, GPT4 & Gemini Pro 模型
[Demo](https://chatgpt.nextweb.fun/) / [Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [Buy Me a Coffee](https://www.buymeacoffee.com/yidadaa)
[![Web][Web-image]][web-url]
[![Windows][Windows-image]][download-url]
[![MacOS][MacOS-image]][download-url]
[![Linux][Linux-image]][download-url]
[演示](https://chatgpt.nextweb.fun/) / [反馈](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [QQ 群](https://github.com/Yidadaa/ChatGPT-Next-Web/discussions/1724) / [打赏开发者](https://user-images.githubusercontent.com/16968934/227772541-5bcd52d8-61b7-488c-a203-0330d8006e2b.jpg)
[Web App](https://app.nextchat.dev/) / [Desktop App](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [Discord](https://discord.gg/YCkeafCafC) / [Twitter](https://twitter.com/NextChatDev)
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web)
[网页版](https://app.nextchat.dev/) / [客户端](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [反馈](https://github.com/Yidadaa/ChatGPT-Next-Web/issues)
[web-url]: https://chatgpt.nextweb.fun
[download-url]: https://github.com/Yidadaa/ChatGPT-Next-Web/releases
[Web-image]: https://img.shields.io/badge/Web-PWA-orange?logo=microsoftedge
[Windows-image]: https://img.shields.io/badge/-Windows-blue?logo=windows
[MacOS-image]: https://img.shields.io/badge/-MacOS-black?logo=apple
[Linux-image]: https://img.shields.io/badge/-Linux-333?logo=ubuntu
[![Deploy with Vercel](https://vercel.com/button)](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)
[![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/templates/ZBUEFA)
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
@@ -24,14 +38,16 @@ One-Click to deploy well-designed ChatGPT web UI on Vercel.
## Features
- **Deploy for free with one-click** on Vercel in under 1 minute
- Privacy first, all data stored locally in the browser
- Compact client (~5MB) on Linux/Windows/MacOS, [download it now](https://github.com/Yidadaa/ChatGPT-Next-Web/releases)
- Fully compatible with self-deployed LLMs, recommended for use with [RWKV-Runner](https://github.com/josStorer/RWKV-Runner) or [LocalAI](https://github.com/go-skynet/LocalAI)
- Privacy first, all data is stored locally in the browser
- Markdown support: LaTex, mermaid, code highlight, etc.
- Responsive design, dark mode and PWA
- Fast first screen loading speed (~100kb), support streaming response
- New in v2: create, share and debug your chat tools with prompt templates (mask)
- Awesome prompts powered by [awesome-chatgpt-prompts-zh](https://github.com/PlexPt/awesome-chatgpt-prompts-zh) and [awesome-chatgpt-prompts](https://github.com/f/awesome-chatgpt-prompts)
- Automatically compresses chat history to support long conversations while also saving your tokens
- I18n: English, 简体中文, 繁体中文, 日本語, Français, Español, Italiano, Türkçe, Deutsch, Tiếng Việt, Русский, Čeština, 한국어
- I18n: English, 简体中文, 繁体中文, 日本語, Français, Español, Italiano, Türkçe, Deutsch, Tiếng Việt, Русский, Čeština, 한국어, Indonesia
## Roadmap
@@ -39,23 +55,22 @@ One-Click to deploy well-designed ChatGPT web UI on Vercel.
- [x] User Prompt: user can edit and save custom prompts to prompt list
- [x] Prompt Template: create a new chat with pre-defined in-context prompts [#993](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/993)
- [x] Share as image, share to ShareGPT [#1741](https://github.com/Yidadaa/ChatGPT-Next-Web/pull/1741)
- [ ] Desktop App with tauri
- [ ] Self-host Model: support llama, alpaca, ChatGLM, BELLE etc.
- [x] Desktop App with tauri
- [x] Self-host Model: Fully compatible with [RWKV-Runner](https://github.com/josStorer/RWKV-Runner), as well as server deployment of [LocalAI](https://github.com/go-skynet/LocalAI): llama/gpt4all/rwkv/vicuna/koala/gpt4all-j/cerebras/falcon/dolly etc.
- [ ] Plugins: support network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
### Not in Plan
- User login, accounts, cloud sync
- UI text customize
## What's New
- 🚀 v2.0 is released, now you can create prompt templates, turn your ideas into reality! Read this: [ChatGPT Prompt Engineering Tips: Zero, One and Few Shot Prompting](https://www.allabtai.com/prompt-engineering-tips-zero-one-and-few-shot-prompting/).
- 🚀 v2.10.1 support Google Gemini Pro model.
- 🚀 v2.9.11 you can use azure endpoint now.
- 🚀 v2.8 now we have a client that runs across all platforms!
- 🚀 v2.7 let's share conversations as image, or share to ShareGPT!
- 🚀 v2.0 is released, now you can create prompt templates, turn your ideas into reality! Read this: [ChatGPT Prompt Engineering Tips: Zero, One and Few Shot Prompting](https://www.allabtai.com/prompt-engineering-tips-zero-one-and-few-shot-prompting/).
## 主要功能
- 在 1 分钟内使用 Vercel **免费一键部署**
- 提供体积极小(~5MB的跨平台客户端Linux/Windows/MacOS, [下载地址](https://github.com/Yidadaa/ChatGPT-Next-Web/releases)
- 完整的 Markdown 支持LaTex 公式、Mermaid 流程图、代码高亮等等
- 精心设计的 UI响应式设计支持深色模式支持 PWA
- 极快的首屏加载速度(~100kb支持流式响应
@@ -63,7 +78,7 @@ One-Click to deploy well-designed ChatGPT web UI on Vercel.
- 预制角色功能(面具),方便地创建、分享和调试你的个性化对话
- 海量的内置 prompt 列表,来自[中文](https://github.com/PlexPt/awesome-chatgpt-prompts-zh)和[英文](https://github.com/f/awesome-chatgpt-prompts)
- 自动压缩上下文聊天记录,在节省 Token 的同时支持超长对话
- 多国语言支持English, 简体中文, 繁体中文, 日本語, Español, Italiano, Türkçe, Deutsch, Tiếng Việt, Русский, Čeština
- 多国语言支持English, 简体中文, 繁体中文, 日本語, Español, Italiano, Türkçe, Deutsch, Tiếng Việt, Русский, Čeština, 한국어, Indonesia
- 拥有自己的域名?好上加好,绑定后即可在任何地方**无障碍**快速访问
## 开发计划
@@ -72,20 +87,17 @@ One-Click to deploy well-designed ChatGPT web UI on Vercel.
- [x] 允许用户自行编辑内置 Prompt 列表
- [x] 预制角色:使用预制角色快速定制新对话 [#993](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/993)
- [x] 分享为图片,分享到 ShareGPT 链接 [#1741](https://github.com/Yidadaa/ChatGPT-Next-Web/pull/1741)
- [ ] 使用 tauri 打包桌面应用
- [ ] 支持自部署的大语言模型
- [x] 使用 tauri 打包桌面应用
- [x] 支持自部署的大语言模型:开箱即用 [RWKV-Runner](https://github.com/josStorer/RWKV-Runner) ,服务端部署 [LocalAI 项目](https://github.com/go-skynet/LocalAI) llama / gpt4all / rwkv / vicuna / koala / gpt4all-j / cerebras / falcon / dolly 等等,或者使用 [api-for-open-llm](https://github.com/xusenlinzy/api-for-open-llm)
- [ ] 插件机制,支持联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
### 不会开发的功能
- 界面文字自定义
- 用户登录、账号管理、消息云同步
## 最新动态
- 🚀 v2.0 已经发布,现在你可以使用面具功能快速创建预制对话了! 了解更多: [ChatGPT 提示词高阶技能:零次、一次和少样本提示](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138)。
- 💡 想要更方便地随时随地使用本项目可以试下这款桌面插件https://github.com/mushan0x0/AI0x0.com
- 🚀 v2.7 现在可以将会话分享为图片了,也可以分享到 ShareGPT 的在线链接。
- 🚀 v2.8 发布了横跨 Linux/Windows/MacOS 的体积极小的客户端。
- 🚀 v2.9.11 现在可以使用自定义 Azure 服务了。
## Get Started
@@ -128,7 +140,7 @@ After forking the project, due to the limitations imposed by GitHub, you need to
If you want to update instantly, you can check out the [GitHub documentation](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork) to learn how to synchronize a forked project with upstream code.
You can star or watch this project or follow author to get release notifictions in time.
You can star or watch this project or follow author to get release notifications in time.
## Access Password
@@ -146,13 +158,13 @@ After adding or modifying this environment variable, please redeploy the project
> [简体中文 > 如何配置 api key、访问密码、接口代理](./README_CN.md#环境变量)
### `OPENAI_API_KEY` (required)
Your openai api key.
### `CODE` (optional)
Access passsword, separated by comma.
Access password, separated by comma.
### `OPENAI_API_KEY` (required)
Your openai api key, join multiple api keys with comma.
### `BASE_URL` (optional)
@@ -166,6 +178,40 @@ Override openai api request base url.
Specify OpenAI organization ID.
### `AZURE_URL` (optional)
> Example: https://{azure-resource-url}/openai/deployments/{deploy-name}
Azure deploy url.
### `AZURE_API_KEY` (optional)
Azure Api Key.
### `AZURE_API_VERSION` (optional)
Azure Api Version, find it at [Azure Documentation](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#chat-completions).
### `GOOGLE_API_KEY` (optional)
Google Gemini Pro Api Key.
### `GOOGLE_URL` (optional)
Google Gemini Pro Api Url.
### `ANTHROPIC_API_KEY` (optional)
anthropic claude Api Key.
### `ANTHROPIC_API_VERSION` (optional)
anthropic claude Api version.
### `ANTHROPIC_URL` (optional)
anthropic claude Api Url.
### `HIDE_USER_API_KEY` (optional)
> Default: Empty
@@ -178,6 +224,38 @@ If you do not want users to input their own API key, set this value to 1.
If you do not want users to use GPT-4, set this value to 1.
### `ENABLE_BALANCE_QUERY` (optional)
> Default: Empty
If you do want users to query balance, set this value to 1.
### `DISABLE_FAST_LINK` (optional)
> Default: Empty
If you want to disable parse settings from url, set this to 1.
### `CUSTOM_MODELS` (optional)
> Default: Empty
> Example: `+llama,+claude-2,-gpt-3.5-turbo,gpt-4-1106-preview=gpt-4-turbo` means add `llama, claude-2` to model list, and remove `gpt-3.5-turbo` from list, and display `gpt-4-1106-preview` as `gpt-4-turbo`.
To control custom models, use `+` to add a custom model, use `-` to hide a model, use `name=displayName` to customize model name, separated by comma.
User `-all` to disable all default models, `+all` to enable all default models.
### `WHITE_WEBDEV_ENDPOINTS` (可选)
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
> `https://xxxx/yyy`
- Multiple addresses are connected by ', '
## Requirements
NodeJS >= 18, Docker >= 20
## Development
> [简体中文 > 如何进行二次开发](./README_CN.md#开发)
@@ -213,8 +291,8 @@ yarn dev
docker pull yidadaa/chatgpt-next-web
docker run -d -p 3000:3000 \
-e OPENAI_API_KEY="sk-xxxx" \
-e CODE="your-password" \
-e OPENAI_API_KEY=sk-xxxx \
-e CODE=your-password \
yidadaa/chatgpt-next-web
```
@@ -222,24 +300,48 @@ You can start service behind a proxy:
```shell
docker run -d -p 3000:3000 \
-e OPENAI_API_KEY="sk-xxxx" \
-e CODE="your-password" \
-e PROXY_URL="http://localhost:7890" \
-e OPENAI_API_KEY=sk-xxxx \
-e CODE=your-password \
-e PROXY_URL=http://localhost:7890 \
yidadaa/chatgpt-next-web
```
If your proxy needs password, use:
```shell
-e PROXY_URL="http://127.0.0.1:7890 user pass"
```
### Shell
```shell
bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/scripts/setup.sh)
```
## Synchronizing Chat Records (UpStash)
| [简体中文](./docs/synchronise-chat-logs-cn.md) | [English](./docs/synchronise-chat-logs-en.md) | [Italiano](./docs/synchronise-chat-logs-es.md) | [日本語](./docs/synchronise-chat-logs-ja.md) | [한국어](./docs/synchronise-chat-logs-ko.md)
## Documentation
> Please go to the [docs][./docs] directory for more documentation instructions.
- [Deploy with cloudflare (Deprecated)](./docs/cloudflare-pages-en.md)
- [Frequent Ask Questions](./docs/faq-en.md)
- [How to add a new translation](./docs/translation.md)
- [How to use Vercel (No English)](./docs/vercel-cn.md)
- [User Manual (Only Chinese, WIP)](./docs/user-manual-cn.md)
## Screenshots
![Settings](./docs/images/settings.png)
![More](./docs/images/more.png)
## Translation
If you want to add a new translation, read this [document](./docs/translation.md).
## Donation
[Buy Me a Coffee](https://www.buymeacoffee.com/yidadaa)
@@ -271,11 +373,19 @@ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/s
[@Sha1rholder](https://github.com/Sha1rholder)
[@AnsonHyq](https://github.com/AnsonHyq)
[@synwith](https://github.com/synwith)
[@piksonGit](https://github.com/piksonGit)
[@ouyangzhiping](https://github.com/ouyangzhiping)
[@wenjiavv](https://github.com/wenjiavv)
[@LeXwDeX](https://github.com/LeXwDeX)
[@Licoy](https://github.com/Licoy)
[@shangmin2009](https://github.com/shangmin2009)
### Contributor
### Contributors
[Contributors](https://github.com/Yidadaa/ChatGPT-Next-Web/graphs/contributors)
<a href="https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/graphs/contributors">
<img src="https://contrib.rocks/image?repo=ChatGPTNextWeb/ChatGPT-Next-Web" />
</a>
## LICENSE
[Anti 996 License](https://github.com/kattgu7/Anti-996-License/blob/master/LICENSE_CN_EN)
[MIT](https://opensource.org/license/mit/)

View File

@@ -1,14 +1,16 @@
<div align="center">
<img src="./docs/images/icon.svg" alt="预览"/>
<h1 align="center">ChatGPT Next Web</h1>
<h1 align="center">NextChat</h1>
一键免费部署你的私人 ChatGPT 网页应用。
一键免费部署你的私人 ChatGPT 网页应用,支持 GPT3, GPT4 & Gemini Pro 模型
[演示 Demo](https://chat-gpt-next-web.vercel.app/) / [反馈 Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [加入 Discord](https://discord.gg/zrhvHCr79N) / [QQ 群](https://user-images.githubusercontent.com/16968934/228190818-7dd00845-e9b9-4363-97e5-44c507ac76da.jpeg) / [打赏开发者](https://user-images.githubusercontent.com/16968934/227772541-5bcd52d8-61b7-488c-a203-0330d8006e2b.jpg) / [Donate](#捐赠-donate-usdt)
[演示 Demo](https://chat-gpt-next-web.vercel.app/) / [反馈 Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [加入 Discord](https://discord.gg/zrhvHCr79N)
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web)
[![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/templates/ZBUEFA)
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
![主界面](./docs/images/cover.png)
@@ -19,7 +21,7 @@
1. 准备好你的 [OpenAI API Key](https://platform.openai.com/account/api-keys);
2. 点击右侧按钮开始部署:
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web),直接使用 Github 账号登录即可,记得在环境变量页填入 API Key 和[页面访问密码](#配置页面访问密码) CODE
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&env=GOOGLE_API_KEY&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web),直接使用 Github 账号登录即可,记得在环境变量页填入 API Key 和[页面访问密码](#配置页面访问密码) CODE
3. 部署完毕后,即可开始使用;
4. (可选)[绑定自定义域名](https://vercel.com/docs/concepts/projects/domains/add-a-domain)Vercel 分配的域名 DNS 在某些区域被污染了,绑定自定义域名即可直连。
@@ -68,7 +70,7 @@ code1,code2,code3
### `OPENAI_API_KEY` (必填项)
OpanAI 密钥,你在 openai 账户页面申请的 api key。
OpanAI 密钥,你在 openai 账户页面申请的 api key使用英文逗号隔开多个 key这样可以随机轮询这些 key。
### `CODE` (可选)
@@ -90,6 +92,40 @@ OpenAI 接口代理 URL如果你手动配置了 openai 接口代理,请填
指定 OpenAI 中的组织 ID。
### `AZURE_URL` (可选)
> 形如https://{azure-resource-url}/openai/deployments/{deploy-name}
Azure 部署地址。
### `AZURE_API_KEY` (可选)
Azure 密钥。
### `AZURE_API_VERSION` (可选)
Azure Api 版本,你可以在这里找到:[Azure 文档](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#chat-completions)。
### `GOOGLE_API_KEY` (optional)
Google Gemini Pro 密钥.
### `GOOGLE_URL` (optional)
Google Gemini Pro Api Url.
### `ANTHROPIC_API_KEY` (optional)
anthropic claude Api Key.
### `ANTHROPIC_API_VERSION` (optional)
anthropic claude Api version.
### `ANTHROPIC_URL` (optional)
anthropic claude Api Url.
### `HIDE_USER_API_KEY` (可选)
如果你不想让用户自行填入 API Key将此环境变量设置为 1 即可。
@@ -98,6 +134,28 @@ OpenAI 接口代理 URL如果你手动配置了 openai 接口代理,请填
如果你不想让用户使用 GPT-4将此环境变量设置为 1 即可。
### `ENABLE_BALANCE_QUERY` (可选)
如果你想启用余额查询功能,将此环境变量设置为 1 即可。
### `DISABLE_FAST_LINK` (可选)
如果你想禁用从链接解析预制设置,将此环境变量设置为 1 即可。
### `WHITE_WEBDEV_ENDPOINTS` (可选)
如果你想增加允许访问的webdav服务地址可以使用该选项格式要求
- 每一个地址必须是一个完整的 endpoint
> `https://xxxx/xxx`
- 多个地址以`,`相连
### `CUSTOM_MODELS` (可选)
> 示例:`+qwen-7b-chat,+glm-6b,-gpt-3.5-turbo,gpt-4-1106-preview=gpt-4-turbo` 表示增加 `qwen-7b-chat` 和 `glm-6b` 到模型列表,而从列表中删除 `gpt-3.5-turbo`,并将 `gpt-4-1106-preview` 模型名字展示为 `gpt-4-turbo`。
> 如果你想先禁用所有模型,再启用指定模型,可以使用 `-all,+gpt-3.5-turbo`,则表示仅启用 `gpt-3.5-turbo`
用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名=展示名` 来自定义模型的展示名,用英文逗号隔开。
## 开发
点击下方按钮,开始二次开发:
@@ -110,14 +168,14 @@ OpenAI 接口代理 URL如果你手动配置了 openai 接口代理,请填
OPENAI_API_KEY=<your api key here>
# 中国大陆用户,可以使用本项目自带的代理进行开发,你也可以自由选择其他代理地址
BASE_URL=https://chatgpt1.nextweb.fun/api/proxy
BASE_URL=https://b.nextweb.fun/api/proxy
```
### 本地开发
1. 安装 nodejs 18 和 yarn具体细节请询问 ChatGPT
2. 执行 `yarn install && yarn dev` 即可。⚠️ 注意:此命令仅用于本地开发,不要用于部署!
3. 如果你想本地部署,请使用 `yarn install && yarn start` 命令,你可以配合 pm2 来守护进程,防止被杀死,详情询问 ChatGPT。
3. 如果你想本地部署,请使用 `yarn install && yarn build && yarn start` 命令,你可以配合 pm2 来守护进程,防止被杀死,详情询问 ChatGPT。
## 部署
@@ -131,8 +189,8 @@ BASE_URL=https://chatgpt1.nextweb.fun/api/proxy
docker pull yidadaa/chatgpt-next-web
docker run -d -p 3000:3000 \
-e OPENAI_API_KEY="sk-xxxx" \
-e CODE="页面访问密码" \
-e OPENAI_API_KEY=sk-xxxx \
-e CODE=页面访问密码 \
yidadaa/chatgpt-next-web
```
@@ -140,13 +198,19 @@ docker run -d -p 3000:3000 \
```shell
docker run -d -p 3000:3000 \
-e OPENAI_API_KEY="sk-xxxx" \
-e CODE="页面访问密码" \
-e OPENAI_API_KEY=sk-xxxx \
-e CODE=页面访问密码 \
--net=host \
-e PROXY_URL="http://127.0.0.1:7890" \
-e PROXY_URL=http://127.0.0.1:7890 \
yidadaa/chatgpt-next-web
```
如果你的本地代理需要账号密码,可以使用:
```shell
-e PROXY_URL="http://127.0.0.1:7890 user password"
```
如果你需要指定其他环境变量,请自行在上述命令中增加 `-e 环境变量=环境变量值` 来指定。
### 本地部署
@@ -169,8 +233,10 @@ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/s
[见项目贡献者列表](https://github.com/Yidadaa/ChatGPT-Next-Web/graphs/contributors)
### 相关项目
- [one-api](https://github.com/songquanpeng/one-api): 一站式大模型额度管理平台,支持市面上所有主流大语言模型
## 开源协议
> 反对 996从我开始。
[Anti 996 License](https://github.com/kattgu7/Anti-996-License/blob/master/LICENSE_CN_EN)
[MIT](https://opensource.org/license/mit/)

View File

@@ -1,171 +0,0 @@
<div align="center">
<img src="./docs/images/icon.svg" alt="预览"/>
<h1 align="center">ChatGPT Next Web</h1>
Implemente su aplicación web privada ChatGPT de forma gratuita con un solo clic.
[Demo demo](https://chat-gpt-next-web.vercel.app/) / [Problemas de comentarios](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [Únete a Discord](https://discord.gg/zrhvHCr79N) / [Grupo QQ](https://user-images.githubusercontent.com/16968934/228190818-7dd00845-e9b9-4363-97e5-44c507ac76da.jpeg) / [Desarrolladores de consejos](https://user-images.githubusercontent.com/16968934/227772541-5bcd52d8-61b7-488c-a203-0330d8006e2b.jpg) / [Donar](#捐赠-donate-usdt)
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web\&env=OPENAI_API_KEY\&env=CODE\&project-name=chatgpt-next-web\&repository-name=ChatGPT-Next-Web)
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
![主界面](./docs/images/cover.png)
</div>
## Comenzar
1. Prepara el tuyo [Clave API OpenAI](https://platform.openai.com/account/api-keys);
2. Haga clic en el botón de la derecha para iniciar la implementación:
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web\&env=OPENAI_API_KEY\&env=CODE\&project-name=chatgpt-next-web\&repository-name=ChatGPT-Next-Web), inicie sesión directamente con su cuenta de Github y recuerde completar la clave API y la suma en la página de variables de entorno[Contraseña de acceso a la página](#配置页面访问密码) CÓDIGO;
3. Una vez implementado, puede comenzar;
4. (Opcional)[Enlazar un nombre de dominio personalizado](https://vercel.com/docs/concepts/projects/domains/add-a-domain): El nombre de dominio DNS asignado por Vercel está contaminado en algunas regiones y puede conectarse directamente enlazando un nombre de dominio personalizado.
## Manténgase actualizado
Si sigue los pasos anteriores para implementar su proyecto con un solo clic, es posible que siempre diga "La actualización existe" porque Vercel creará un nuevo proyecto para usted de forma predeterminada en lugar de bifurcar el proyecto, lo que evitará que la actualización se detecte correctamente.
Le recomendamos que siga estos pasos para volver a implementar:
* Eliminar el repositorio original;
* Utilice el botón de bifurcación en la esquina superior derecha de la página para bifurcar este proyecto;
* En Vercel, vuelva a seleccionar e implementar,[Echa un vistazo al tutorial detallado](./docs/vercel-cn.md#如何新建项目)。
### Activar actualizaciones automáticas
> Si encuentra un error de ejecución de Upstream Sync, ¡Sync Fork manualmente una vez!
Cuando bifurca el proyecto, debido a las limitaciones de Github, debe ir manualmente a la página Acciones de su proyecto bifurcado para habilitar Flujos de trabajo y habilitar Upstream Sync Action, después de habilitarlo, puede activar las actualizaciones automáticas cada hora:
![自动更新](./docs/images/enable-actions.jpg)
![启用自动更新](./docs/images/enable-actions-sync.jpg)
### Actualizar el código manualmente
Si desea que el manual se actualice inmediatamente, puede consultarlo [Documentación para Github](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork) Aprenda a sincronizar un proyecto bifurcado con código ascendente.
Puede destacar / ver este proyecto o seguir al autor para recibir notificaciones de nuevas actualizaciones de funciones.
## Configurar la contraseña de acceso a la página
> Después de configurar la contraseña, el usuario debe completar manualmente el código de acceso en la página de configuración para chatear normalmente, de lo contrario, se solicitará el estado no autorizado a través de un mensaje.
> **advertir**: Asegúrese de establecer el número de dígitos de la contraseña lo suficientemente largo, preferiblemente más de 7 dígitos, de lo contrario[Será volado](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/518)。
Este proyecto proporciona control de permisos limitado, agregue el nombre al nombre en la página Variables de entorno del Panel de control del proyecto Vercel `CODE` Variables de entorno con valores para contraseñas personalizadas separadas por comas:
code1,code2,code3
Después de agregar o modificar la variable de entorno, por favor**Redesplegar**proyecto para poner en vigor los cambios.
## Variable de entorno
> La mayoría de los elementos de configuración de este proyecto se establecen a través de variables de entorno, tutorial:[Cómo modificar las variables de entorno de Vercel](./docs/vercel-cn.md)。
### `OPENAI_API_KEY` (Requerido)
OpanAI key, la clave API que solicita en la página de su cuenta openai.
### `CODE` (Opcional)
Las contraseñas de acceso, opcionalmente, se pueden separar por comas.
**advertir**: Si no completa este campo, cualquiera puede usar directamente su sitio web implementado, lo que puede hacer que su token se consuma rápidamente, se recomienda completar esta opción.
### `BASE_URL` (Opcional)
> Predeterminado: `https://api.openai.com`
> Ejemplos: `http://your-openai-proxy.com`
URL del proxy de interfaz OpenAI, complete esta opción si configuró manualmente el proxy de interfaz openAI.
> Si encuentra problemas con el certificado SSL, establezca el `BASE_URL` El protocolo se establece en http.
### `OPENAI_ORG_ID` (Opcional)
Especifica el identificador de la organización en OpenAI.
### `HIDE_USER_API_KEY` (Opcional)
Si no desea que los usuarios rellenen la clave de API ellos mismos, establezca esta variable de entorno en 1.
### `DISABLE_GPT4` (Opcional)
Si no desea que los usuarios utilicen GPT-4, establezca esta variable de entorno en 1.
## explotación
> No se recomienda encarecidamente desarrollar o implementar localmente, debido a algunas razones técnicas, es difícil configurar el agente API de OpenAI localmente, a menos que pueda asegurarse de que puede conectarse directamente al servidor OpenAI.
Haga clic en el botón de abajo para iniciar el desarrollo secundario:
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
Antes de empezar a escribir código, debe crear uno nuevo en la raíz del proyecto `.env.local` archivo, lleno de variables de entorno:
OPENAI_API_KEY=<your api key here>
### Desarrollo local
1. Instale nodejs 18 e hilo, pregunte a ChatGPT para obtener más detalles;
2. ejecutar `yarn install && yarn dev` Enlatar. ⚠️ Nota: Este comando es solo para desarrollo local, no para implementación.
3. Úselo si desea implementar localmente `yarn install && yarn start` comando, puede cooperar con pm2 a daemon para evitar ser asesinado, pregunte a ChatGPT para obtener más detalles.
## desplegar
### Implementación de contenedores (recomendado)
> La versión de Docker debe ser 20 o posterior, de lo contrario se indicará que no se puede encontrar la imagen.
> ⚠️ Nota: Las versiones de Docker están de 1 a 2 días por detrás de la última versión la mayor parte del tiempo, por lo que es normal que sigas diciendo "La actualización existe" después de la implementación.
```shell
docker pull yidadaa/chatgpt-next-web
docker run -d -p 3000:3000 \
-e OPENAI_API_KEY="sk-xxxx" \
-e CODE="页面访问密码" \
yidadaa/chatgpt-next-web
```
También puede especificar proxy:
```shell
docker run -d -p 3000:3000 \
-e OPENAI_API_KEY="sk-xxxx" \
-e CODE="页面访问密码" \
--net=host \
-e PROXY_URL="http://127.0.0.1:7890" \
yidadaa/chatgpt-next-web
```
Si necesita especificar otras variables de entorno, agréguelas usted mismo en el comando anterior `-e 环境变量=环境变量值` para especificar.
### Implementación local
Ejecute el siguiente comando en la consola:
```shell
bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/scripts/setup.sh)
```
⚠️ Nota: Si tiene problemas durante la instalación, utilice la implementación de Docker.
## Reconocimiento
### donante
> Ver versión en inglés.
### Colaboradores
[Ver la lista de colaboradores del proyecto](https://github.com/Yidadaa/ChatGPT-Next-Web/graphs/contributors)
## Licencia de código abierto
> Contra 996, empezando por mí.
[Licencia Anti 996](https://github.com/kattgu7/Anti-996-License/blob/master/LICENSE_CN_EN)

102
app/(app)/chat/layout.tsx Normal file
View File

@@ -0,0 +1,102 @@
"use client";
import {
DEFAULT_SIDEBAR_WIDTH,
MAX_SIDEBAR_WIDTH,
MIN_SIDEBAR_WIDTH,
Path,
} from "@/app/constant";
import useDrag from "@/app/hooks/useDrag";
import useMobileScreen from "@/app/hooks/useMobileScreen";
import { updateGlobalCSSVars } from "@/app/utils/client";
import { useRef, useState } from "react";
import { useAppConfig } from "@/app/store/config";
import React from "react";
import { AuthPage } from "@/app/components/auth";
import { SideBar } from "@/app/containers/Sidebar";
import Screen from "@/app/components/Screen";
import { useSwitchTheme } from "@/app/hooks/useSwitchTheme";
import Chat from "@/app/containers/Chat/ChatPanel";
export default function Layout({ children }: { children: React.ReactNode }) {
const [showPanel, setShowPanel] = useState(false);
const [externalProps, setExternalProps] = useState({});
const config = useAppConfig();
useSwitchTheme();
const isMobileScreen = useMobileScreen();
const startDragWidth = useRef(config.sidebarWidth ?? DEFAULT_SIDEBAR_WIDTH);
// drag side bar
const { onDragStart } = useDrag({
customToggle: () => {
config.update((config) => {
config.sidebarWidth = DEFAULT_SIDEBAR_WIDTH;
});
},
customDragMove: (nextWidth: number) => {
const { menuWidth } = updateGlobalCSSVars(nextWidth);
document.documentElement.style.setProperty(
"--menu-width",
`${menuWidth}px`,
);
config.update((config) => {
config.sidebarWidth = nextWidth;
});
},
customLimit: (x: number) =>
Math.max(
MIN_SIDEBAR_WIDTH,
Math.min(MAX_SIDEBAR_WIDTH, startDragWidth.current + x),
),
});
return (
<div
className={`
w-[100%] relative bg-center
max-md:h-[100%]
md:flex md:my-2.5
`}
>
<div
className={`
flex flex-col px-6
h-[100%]
max-md:w-[100%] max-md:px-4 max-md:pb-4 max-md:flex-1
md:relative md:basis-sidebar md:pb-6 md:rounded-md md:bg-menu
`}
>
{children}
</div>
{!isMobileScreen && (
<div
className={`group/menu-dragger cursor-col-resize w-[0.25rem] flex items-center justify-center`}
onPointerDown={(e) => {
startDragWidth.current = config.sidebarWidth;
onDragStart(e as any);
}}
>
<div className="w-[2px] opacity-0 group-hover/menu-dragger:opacity-100 bg-menu-dragger h-[100%] rounded-[2px]">
&nbsp;
</div>
</div>
)}
<div
className={`
md:flex-1 md:h-[100%] md:w-page
max-md:transition-all max-md:duration-300 max-md:absolute max-md:top-0 max-md:max-h-[100vh] max-md:w-[100%] ${
showPanel ? "max-md:left-0" : "max-md:left-[101%]"
} max-md:z-10
`}
>
{/* <PanelComponent
{...props}
{...externalProps}
setShowPanel={setShowPanel}
setExternalProps={setExternalProps}
showPanel={showPanel}
/> */}
{/* {children} */}
<Chat></Chat>
</div>
</div>
);
}

137
app/(app)/chat/page.tsx Normal file
View File

@@ -0,0 +1,137 @@
"use client";
import {
DragDropContext,
Droppable,
OnDragEndResponder,
} from "@hello-pangea/dnd";
import { useAppConfig, useChatStore } from "@/app/store";
import Locale from "@/app/locales";
// import { useLocation, useNavigate } from "react-router-dom";
import { useRouter, usePathname } from "next/navigation";
import AddIcon from "@/app/icons/addIcon.svg";
import NextChatTitle from "@/app/icons/nextchatTitle.svg";
import Modal from "@/app/components/Modal";
import SessionItem from "@/app/containers/Chat/components/SessionItem";
export default function Page() {
const [sessions, selectedIndex, selectSession, moveSession] = useChatStore(
(state) => [
state.sessions,
state.currentSessionIndex,
state.selectSession,
state.moveSession,
],
);
const config = useAppConfig();
const { isMobileScreen } = config;
const chatStore = useChatStore();
const pathname = usePathname();
const onDragEnd: OnDragEndResponder = (result) => {
const { destination, source } = result;
if (!destination) {
return;
}
if (
destination.droppableId === source.droppableId &&
destination.index === source.index
) {
return;
}
moveSession(source.index, destination.index);
};
return (
<div
className={`
h-[100%] flex flex-col
md:px-0
`}
>
<div data-tauri-drag-region>
<div
className={`
flex items-center justify-between
py-6 max-md:box-content max-md:h-0
md:py-7
`}
data-tauri-drag-region
>
<div className="">
<NextChatTitle />
</div>
<div
className="cursor-pointer "
onClick={() => {
// if (config.dontShowMaskSplashScreen) {
// chatStore.newSession();
// navigate(Path.Chat);
// } else {
// navigate(Path.NewChat);
// }
}}
>
<AddIcon />
</div>
</div>
<div
className={`pb-3 text-sm sm:text-sm-mobile text-text-chat-header-subtitle`}
>
Build your own AI assistant.
</div>
</div>
<div className={`flex-1 overflow-y-auto max-md:pb-chat-panel-mobile `}>
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="chat-list">
{(provided) => (
<div
ref={provided.innerRef}
{...provided.droppableProps}
className={`w-[100%]`}
>
{sessions.map((item, i) => (
<SessionItem
title={item.topic}
time={new Date(item.lastUpdate).toLocaleString()}
count={item.messages.length}
key={item.id}
id={item.id}
index={i}
selected={i === selectedIndex}
onClick={() => {
// navigate(Path.Chat);
// selectSession(i);
}}
onDelete={async () => {
if (
await Modal.warn({
okText: Locale.ChatItem.DeleteOkBtn,
cancelText: Locale.ChatItem.DeleteCancelBtn,
title: Locale.ChatItem.DeleteTitle,
content: Locale.ChatItem.DeleteContent,
})
) {
chatStore.deleteSession(i);
}
}}
mask={item.mask}
isMobileScreen={isMobileScreen}
/>
))}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
</div>
</div>
);
}

21
app/(app)/layout.tsx Normal file
View File

@@ -0,0 +1,21 @@
"use client";
import React from "react";
import { AuthPage } from "@/app/components/auth";
import { SideBar } from "@/app/containers/Sidebar";
import Screen from "@/app/components/Screen";
export interface MenuWrapperInspectProps {
setExternalProps?: (v: Record<string, any>) => void;
setShowPanel?: (v: boolean) => void;
showPanel?: boolean;
[k: string]: any;
}
export default function AppLayout({ children }: { children: React.ReactNode }) {
return (
<Screen noAuth={<AuthPage />} sidebar={<SideBar />}>
{children}
</Screen>
);
}

View File

@@ -0,0 +1,4 @@
import React from "react";
export default function Layout({ children }: { children: React.ReactNode }) {
return <>{children}</>;
}

View File

@@ -0,0 +1,3 @@
export default function Page() {
return <></>;
}

View File

@@ -0,0 +1,189 @@
import { getServerSideConfig } from "@/app/config/server";
import {
ANTHROPIC_BASE_URL,
Anthropic,
ApiPath,
DEFAULT_MODELS,
ModelProvider,
} from "@/app/constant";
import { prettyObject } from "@/app/utils/format";
import { NextRequest, NextResponse } from "next/server";
import { auth } from "../../auth";
import { collectModelTable } from "@/app/utils/model";
const ALLOWD_PATH = new Set([Anthropic.ChatPath, Anthropic.ChatPath1]);
async function handle(
req: NextRequest,
{ params }: { params: { path: string[] } },
) {
console.log("[Anthropic Route] params ", params);
if (req.method === "OPTIONS") {
return NextResponse.json({ body: "OK" }, { status: 200 });
}
const subpath = params.path.join("/");
if (!ALLOWD_PATH.has(subpath)) {
console.log("[Anthropic Route] forbidden path ", subpath);
return NextResponse.json(
{
error: true,
msg: "you are not allowed to request " + subpath,
},
{
status: 403,
},
);
}
const authResult = auth(req, ModelProvider.Claude);
if (authResult.error) {
return NextResponse.json(authResult, {
status: 401,
});
}
try {
const response = await request(req);
return response;
} catch (e) {
console.error("[Anthropic] ", e);
return NextResponse.json(prettyObject(e));
}
}
export const GET = handle;
export const POST = handle;
export const runtime = "edge";
export const preferredRegion = [
"arn1",
"bom1",
"cdg1",
"cle1",
"cpt1",
"dub1",
"fra1",
"gru1",
"hnd1",
"iad1",
"icn1",
"kix1",
"lhr1",
"pdx1",
"sfo1",
"sin1",
"syd1",
];
const serverConfig = getServerSideConfig();
async function request(req: NextRequest) {
const controller = new AbortController();
let authHeaderName = "x-api-key";
let authValue =
req.headers.get(authHeaderName) ||
req.headers.get("Authorization")?.replaceAll("Bearer ", "").trim() ||
serverConfig.anthropicApiKey ||
"";
let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Anthropic, "");
let baseUrl =
serverConfig.anthropicUrl || serverConfig.baseUrl || ANTHROPIC_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",
"Cache-Control": "no-store",
[authHeaderName]: authValue,
"anthropic-version":
req.headers.get("anthropic-version") ||
serverConfig.anthropicApiVersion ||
Anthropic.Vision,
},
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 modelTable = collectModelTable(
DEFAULT_MODELS,
serverConfig.customModels,
);
const clonedBody = await req.text();
fetchOptions.body = clonedBody;
const jsonBody = JSON.parse(clonedBody) as { model?: string };
// not undefined and is false
if (modelTable[jsonBody?.model ?? ""].available === false) {
return NextResponse.json(
{
error: true,
message: `you are not allowed to use ${jsonBody?.model} model`,
},
{
status: 403,
},
);
}
} catch (e) {
console.error(`[Anthropic] filter`, e);
}
}
console.log("[Anthropic request]", fetchOptions.headers, req.method);
try {
const res = await fetch(fetchUrl, fetchOptions);
console.log(
"[Anthropic response]",
res.status,
" ",
res.headers,
res.url,
);
// 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,7 @@
import { NextRequest } from "next/server";
import { getServerSideConfig } from "../config/server";
import md5 from "spark-md5";
import { ACCESS_CODE_PREFIX } from "../constant";
import { ACCESS_CODE_PREFIX, ModelProvider } from "../constant";
function getIP(req: NextRequest) {
let ip = req.ip ?? req.headers.get("x-real-ip");
@@ -16,19 +16,19 @@ function getIP(req: NextRequest) {
function parseApiKey(bearToken: string) {
const token = bearToken.trim().replaceAll("Bearer ", "").trim();
const isOpenAiKey = !token.startsWith(ACCESS_CODE_PREFIX);
const isApiKey = !token.startsWith(ACCESS_CODE_PREFIX);
return {
accessCode: isOpenAiKey ? "" : token.slice(ACCESS_CODE_PREFIX.length),
apiKey: isOpenAiKey ? token : "",
accessCode: isApiKey ? "" : token.slice(ACCESS_CODE_PREFIX.length),
apiKey: isApiKey ? token : "",
};
}
export function auth(req: NextRequest) {
export function auth(req: NextRequest, modelProvider: ModelProvider) {
const authToken = req.headers.get("Authorization") ?? "";
// check if it is openai api key or user token
const { accessCode, apiKey: token } = parseApiKey(authToken);
const { accessCode, apiKey } = parseApiKey(authToken);
const hashedCode = md5.hash(accessCode ?? "").trim();
@@ -39,19 +39,52 @@ export function auth(req: NextRequest) {
console.log("[User IP] ", getIP(req));
console.log("[Time] ", new Date().toLocaleString());
if (serverConfig.needCode && !serverConfig.codes.has(hashedCode) && !token) {
if (serverConfig.needCode && !serverConfig.codes.has(hashedCode) && !apiKey) {
return {
error: true,
msg: !accessCode ? "empty access code" : "wrong access code",
};
}
if (serverConfig.hideUserApiKey && !!apiKey) {
return {
error: true,
msg: "you are not allowed to access with your own api key",
};
}
// if user does not provide an api key, inject system api key
if (!token) {
const apiKey = serverConfig.apiKey;
if (apiKey) {
if (!apiKey) {
const serverConfig = getServerSideConfig();
// const systemApiKey =
// modelProvider === ModelProvider.GeminiPro
// ? serverConfig.googleApiKey
// : serverConfig.isAzure
// ? serverConfig.azureApiKey
// : serverConfig.apiKey;
let systemApiKey: string | undefined;
switch (modelProvider) {
case ModelProvider.GeminiPro:
systemApiKey = serverConfig.googleApiKey;
break;
case ModelProvider.Claude:
systemApiKey = serverConfig.anthropicApiKey;
break;
case ModelProvider.GPT:
default:
if (serverConfig.isAzure) {
systemApiKey = serverConfig.azureApiKey;
} else {
systemApiKey = serverConfig.apiKey;
}
}
if (systemApiKey) {
console.log("[Auth] use system api key");
req.headers.set("Authorization", `Bearer ${apiKey}`);
req.headers.set("Authorization", `Bearer ${systemApiKey}`);
} else {
console.log("[Auth] admin did not provide an api key");
}

View File

@@ -1,64 +1,103 @@
import { NextRequest, NextResponse } from "next/server";
import { getServerSideConfig } from "../config/server";
import { DEFAULT_MODELS, OPENAI_BASE_URL, GEMINI_BASE_URL } from "../constant";
import { collectModelTable } from "../utils/model";
import { makeAzurePath } from "../azure";
export const OPENAI_URL = "api.openai.com";
const DEFAULT_PROTOCOL = "https";
const PROTOCOL = process.env.PROTOCOL ?? DEFAULT_PROTOCOL;
const BASE_URL = process.env.BASE_URL ?? OPENAI_URL;
const DISABLE_GPT4 = !!process.env.DISABLE_GPT4;
const serverConfig = getServerSideConfig();
export async function requestOpenai(req: NextRequest) {
const controller = new AbortController();
const authValue = req.headers.get("Authorization") ?? "";
const openaiPath = `${req.nextUrl.pathname}${req.nextUrl.search}`.replaceAll(
var authValue,
authHeaderName = "";
if (serverConfig.isAzure) {
authValue =
req.headers
.get("Authorization")
?.trim()
.replaceAll("Bearer ", "")
.trim() ?? "";
authHeaderName = "api-key";
} else {
authValue = req.headers.get("Authorization") ?? "";
authHeaderName = "Authorization";
}
let path = `${req.nextUrl.pathname}${req.nextUrl.search}`.replaceAll(
"/api/openai/",
"",
);
let baseUrl = BASE_URL;
let baseUrl =
serverConfig.azureUrl || serverConfig.baseUrl || OPENAI_BASE_URL;
if (!baseUrl.startsWith("http")) {
baseUrl = `${PROTOCOL}://${baseUrl}`;
baseUrl = `https://${baseUrl}`;
}
console.log("[Proxy] ", openaiPath);
if (baseUrl.endsWith("/")) {
baseUrl = baseUrl.slice(0, -1);
}
console.log("[Proxy] ", path);
console.log("[Base Url]", baseUrl);
if (process.env.OPENAI_ORG_ID) {
console.log("[Org ID]", process.env.OPENAI_ORG_ID);
const timeoutId = setTimeout(
() => {
controller.abort();
},
10 * 60 * 1000,
);
if (serverConfig.isAzure) {
if (!serverConfig.azureApiVersion) {
return NextResponse.json({
error: true,
message: `missing AZURE_API_VERSION in server env vars`,
});
}
path = makeAzurePath(path, serverConfig.azureApiVersion);
}
const timeoutId = setTimeout(() => {
controller.abort();
}, 10 * 60 * 1000);
const fetchUrl = `${baseUrl}/${openaiPath}`;
const fetchUrl = `${baseUrl}/${path}`;
const fetchOptions: RequestInit = {
headers: {
"Content-Type": "application/json",
Authorization: authValue,
...(process.env.OPENAI_ORG_ID && {
"OpenAI-Organization": process.env.OPENAI_ORG_ID,
"Cache-Control": "no-store",
[authHeaderName]: authValue,
...(serverConfig.openaiOrgId && {
"OpenAI-Organization": serverConfig.openaiOrgId,
}),
},
cache: "no-store",
method: req.method,
body: req.body,
// to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body
redirect: "manual",
// @ts-ignore
duplex: "half",
signal: controller.signal,
};
// #1815 try to refuse gpt4 request
if (DISABLE_GPT4 && req.body) {
if (serverConfig.customModels && req.body) {
try {
const modelTable = collectModelTable(
DEFAULT_MODELS,
serverConfig.customModels,
);
const clonedBody = await req.text();
fetchOptions.body = clonedBody;
const jsonBody = JSON.parse(clonedBody);
const jsonBody = JSON.parse(clonedBody) as { model?: string };
if ((jsonBody?.model ?? "").includes("gpt-4")) {
// not undefined and is false
if (modelTable[jsonBody?.model ?? ""].available === false) {
return NextResponse.json(
{
error: true,
message: "you are not allowed to use gpt-4 model",
message: `you are not allowed to use ${jsonBody?.model} model`,
},
{
status: 403,
@@ -73,13 +112,37 @@ export async function requestOpenai(req: NextRequest) {
try {
const res = await fetch(fetchUrl, fetchOptions);
// Extract the OpenAI-Organization header from the response
const openaiOrganizationHeader = res.headers.get("OpenAI-Organization");
// Check if serverConfig.openaiOrgId is defined and not an empty string
if (serverConfig.openaiOrgId && serverConfig.openaiOrgId.trim() !== "") {
// If openaiOrganizationHeader is present, log it; otherwise, log that the header is not present
console.log("[Org ID]", openaiOrganizationHeader);
} else {
console.log("[Org ID] is not set up.");
}
// to prevent browser prompt for credentials
const newHeaders = new Headers(res.headers);
newHeaders.delete("www-authenticate");
// to disbale ngnix buffering
// to disable nginx buffering
newHeaders.set("X-Accel-Buffering", "no");
// Conditionally delete the OpenAI-Organization header from the response if [Org ID] is undefined or empty (not setup in ENV)
// Also, this is to prevent the header from being sent to the client
if (!serverConfig.openaiOrgId || serverConfig.openaiOrgId.trim() === "") {
newHeaders.delete("OpenAI-Organization");
}
// The latest version of the OpenAI API forced the content-encoding to be "br" in json response
// So if the streaming is disabled, we need to remove the content-encoding header
// Because Vercel uses gzip to compress the response, if we don't remove the content-encoding header
// The browser will try to decode the response with brotli and fail
newHeaders.delete("content-encoding");
return new Response(res.body, {
status: res.status,
statusText: res.statusText,

View File

@@ -4,12 +4,16 @@ import { getServerSideConfig } from "../../config/server";
const serverConfig = getServerSideConfig();
// Danger! Don not write any secret value here!
// Danger! Do not hard code any secret value here!
// 警告!不要在这里写入任何敏感信息!
const DANGER_CONFIG = {
needCode: serverConfig.needCode,
hideUserApiKey: serverConfig.hideUserApiKey,
enableGPT4: serverConfig.enableGPT4,
disableGPT4: serverConfig.disableGPT4,
hideBalanceQuery: serverConfig.hideBalanceQuery,
disableFastLink: serverConfig.disableFastLink,
customModels: serverConfig.customModels,
defaultModel: serverConfig.defaultModel,
};
declare global {

View File

@@ -0,0 +1,116 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "../../auth";
import { getServerSideConfig } from "@/app/config/server";
import { GEMINI_BASE_URL, Google, ModelProvider } from "@/app/constant";
async function handle(
req: NextRequest,
{ params }: { params: { path: string[] } },
) {
console.log("[Google Route] params ", params);
if (req.method === "OPTIONS") {
return NextResponse.json({ body: "OK" }, { status: 200 });
}
const controller = new AbortController();
const serverConfig = getServerSideConfig();
let baseUrl = serverConfig.googleUrl || GEMINI_BASE_URL;
if (!baseUrl.startsWith("http")) {
baseUrl = `https://${baseUrl}`;
}
if (baseUrl.endsWith("/")) {
baseUrl = baseUrl.slice(0, -1);
}
let path = `${req.nextUrl.pathname}`.replaceAll("/api/google/", "");
console.log("[Proxy] ", path);
console.log("[Base Url]", baseUrl);
const timeoutId = setTimeout(
() => {
controller.abort();
},
10 * 60 * 1000,
);
const authResult = auth(req, ModelProvider.GeminiPro);
if (authResult.error) {
return NextResponse.json(authResult, {
status: 401,
});
}
const bearToken = req.headers.get("Authorization") ?? "";
const token = bearToken.trim().replaceAll("Bearer ", "").trim();
const key = token ? token : serverConfig.googleApiKey;
if (!key) {
return NextResponse.json(
{
error: true,
message: `missing GOOGLE_API_KEY in server env vars`,
},
{
status: 401,
},
);
}
const fetchUrl = `${baseUrl}/${path}?key=${key}`;
const fetchOptions: RequestInit = {
headers: {
"Content-Type": "application/json",
"Cache-Control": "no-store",
},
method: req.method,
body: req.body,
// to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body
redirect: "manual",
// @ts-ignore
duplex: "half",
signal: controller.signal,
};
try {
const res = await fetch(fetchUrl, fetchOptions);
// to prevent browser prompt for credentials
const newHeaders = new Headers(res.headers);
newHeaders.delete("www-authenticate");
// to disable nginx buffering
newHeaders.set("X-Accel-Buffering", "no");
return new Response(res.body, {
status: res.status,
statusText: res.statusText,
headers: newHeaders,
});
} finally {
clearTimeout(timeoutId);
}
}
export const GET = handle;
export const POST = handle;
export const runtime = "edge";
export const preferredRegion = [
"bom1",
"cle1",
"cpt1",
"gru1",
"hnd1",
"iad1",
"icn1",
"kix1",
"pdx1",
"sfo1",
"sin1",
"syd1",
];

View File

@@ -1,4 +1,6 @@
import { OpenaiPath } from "@/app/constant";
import { type OpenAIListModelResponse } from "@/app/client/platforms/openai";
import { getServerSideConfig } from "@/app/config/server";
import { ModelProvider, OpenaiPath } from "@/app/constant";
import { prettyObject } from "@/app/utils/format";
import { NextRequest, NextResponse } from "next/server";
import { auth } from "../../auth";
@@ -6,6 +8,18 @@ import { requestOpenai } from "../../common";
const ALLOWD_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"),
);
}
return remoteModelRes;
}
async function handle(
req: NextRequest,
{ params }: { params: { path: string[] } },
@@ -31,7 +45,7 @@ async function handle(
);
}
const authResult = auth(req);
const authResult = auth(req, ModelProvider.GPT);
if (authResult.error) {
return NextResponse.json(authResult, {
status: 401,
@@ -39,7 +53,18 @@ async function handle(
}
try {
return await requestOpenai(req);
const response = await requestOpenai(req);
// list models
if (subpath === OpenaiPath.ListModelPath && response.status === 200) {
const resJson = (await response.json()) as OpenAIListModelResponse;
const availableModels = getModels(resJson);
return NextResponse.json(availableModels, {
status: response.status,
});
}
return response;
} catch (e) {
console.error("[OpenAI] ", e);
return NextResponse.json(prettyObject(e));
@@ -50,3 +75,22 @@ export const GET = handle;
export const POST = handle;
export const runtime = "edge";
export const preferredRegion = [
"arn1",
"bom1",
"cdg1",
"cle1",
"cpt1",
"dub1",
"fra1",
"gru1",
"hnd1",
"iad1",
"icn1",
"kix1",
"lhr1",
"pdx1",
"sfo1",
"sin1",
"syd1",
];

View File

@@ -0,0 +1,73 @@
import { NextRequest, NextResponse } from "next/server";
async function handle(
req: NextRequest,
{ params }: { params: { action: string; key: string[] } },
) {
const requestUrl = new URL(req.url);
const endpoint = requestUrl.searchParams.get("endpoint");
if (req.method === "OPTIONS") {
return NextResponse.json({ body: "OK" }, { status: 200 });
}
const [...key] = params.key;
// only allow to request to *.upstash.io
if (!endpoint || !new URL(endpoint).hostname.endsWith(".upstash.io")) {
return NextResponse.json(
{
error: true,
msg: "you are not allowed to request " + params.key.join("/"),
},
{
status: 403,
},
);
}
// only allow upstash get and set method
if (params.action !== "get" && params.action !== "set") {
console.log("[Upstash Route] forbidden action ", params.action);
return NextResponse.json(
{
error: true,
msg: "you are not allowed to request " + params.action,
},
{
status: 403,
},
);
}
const targetUrl = `${endpoint}/${params.action}/${params.key.join("/")}`;
const method = req.method;
const shouldNotHaveBody = ["get", "head"].includes(
method?.toLowerCase() ?? "",
);
const fetchOptions: RequestInit = {
headers: {
authorization: req.headers.get("authorization") ?? "",
},
body: shouldNotHaveBody ? null : req.body,
method,
// @ts-ignore
duplex: "half",
};
console.log("[Upstash Proxy]", targetUrl, fetchOptions);
const fetchResult = await fetch(targetUrl, fetchOptions);
console.log("[Any Proxy]", targetUrl, {
status: fetchResult.status,
statusText: fetchResult.statusText,
});
return fetchResult;
}
export const POST = handle;
export const GET = handle;
export const OPTIONS = handle;
export const runtime = "edge";

View File

@@ -0,0 +1,142 @@
import { NextRequest, NextResponse } from "next/server";
import { STORAGE_KEY, internalWhiteWebDavEndpoints } from "../../../constant";
import { getServerSideConfig } from "@/app/config/server";
const config = getServerSideConfig();
const mergedWhiteWebDavEndpoints = [
...internalWhiteWebDavEndpoints,
...config.whiteWebDevEndpoints,
].filter((domain) => Boolean(domain.trim()));
async function handle(
req: NextRequest,
{ params }: { params: { path: string[] } },
) {
if (req.method === "OPTIONS") {
return NextResponse.json({ body: "OK" }, { status: 200 });
}
const folder = STORAGE_KEY;
const fileName = `${folder}/backup.json`;
const requestUrl = new URL(req.url);
let endpoint = requestUrl.searchParams.get("endpoint");
// Validate the endpoint to prevent potential SSRF attacks
if (
!mergedWhiteWebDavEndpoints.some((white) => endpoint?.startsWith(white))
) {
return NextResponse.json(
{
error: true,
msg: "Invalid endpoint",
},
{
status: 400,
},
);
}
if (!endpoint?.endsWith("/")) {
endpoint += "/";
}
const endpointPath = params.path.join("/");
const targetPath = `${endpoint}${endpointPath}`;
// only allow MKCOL, GET, PUT
if (req.method !== "MKCOL" && req.method !== "GET" && req.method !== "PUT") {
return NextResponse.json(
{
error: true,
msg: "you are not allowed to request " + targetPath,
},
{
status: 403,
},
);
}
// for MKCOL request, only allow request ${folder}
if (req.method === "MKCOL" && !targetPath.endsWith(folder)) {
return NextResponse.json(
{
error: true,
msg: "you are not allowed to request " + targetPath,
},
{
status: 403,
},
);
}
// for GET request, only allow request ending with fileName
if (req.method === "GET" && !targetPath.endsWith(fileName)) {
return NextResponse.json(
{
error: true,
msg: "you are not allowed to request " + targetPath,
},
{
status: 403,
},
);
}
// for PUT request, only allow request ending with fileName
if (req.method === "PUT" && !targetPath.endsWith(fileName)) {
return NextResponse.json(
{
error: true,
msg: "you are not allowed to request " + targetPath,
},
{
status: 403,
},
);
}
const targetUrl = targetPath;
const method = req.method;
const shouldNotHaveBody = ["get", "head"].includes(
method?.toLowerCase() ?? "",
);
const fetchOptions: RequestInit = {
headers: {
authorization: req.headers.get("authorization") ?? "",
},
body: shouldNotHaveBody ? null : req.body,
redirect: "manual",
method,
// @ts-ignore
duplex: "half",
};
let fetchResult;
try {
fetchResult = await fetch(targetUrl, fetchOptions);
} finally {
console.log(
"[Any Proxy]",
targetUrl,
{
method: req.method,
},
{
status: fetchResult?.status,
statusText: fetchResult?.statusText,
},
);
}
return fetchResult;
}
export const PUT = handle;
export const GET = handle;
export const OPTIONS = handle;
export const runtime = "edge";

9
app/azure.ts Normal file
View File

@@ -0,0 +1,9 @@
export function makeAzurePath(path: string, apiVersion: string) {
// should omit /v1 prefix
path = path.replaceAll("v1/", "");
// should add api-key to query string
path += `${path.includes("?") ? "&" : "?"}api-version=${apiVersion}`;
return path;
}

View File

@@ -1,17 +1,31 @@
import { getClientConfig } from "../config/client";
import { ACCESS_CODE_PREFIX } from "../constant";
import { ChatMessage, ModelType, useAccessStore } from "../store";
import {
ACCESS_CODE_PREFIX,
Azure,
ModelProvider,
ServiceProvider,
} from "../constant";
import { ChatMessage, ModelType, useAccessStore, useChatStore } from "../store";
import { ChatGPTApi } from "./platforms/openai";
import { GeminiProApi } from "./platforms/google";
import { ClaudeApi } from "./platforms/anthropic";
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 type ChatModel = ModelType;
export interface MultimodalContent {
type: "text" | "image_url";
text?: string;
image_url?: {
url: string;
};
}
export interface RequestMessage {
role: MessageRole;
content: string;
content: string | MultimodalContent[];
}
export interface LLMConfig {
@@ -38,9 +52,22 @@ export interface LLMUsage {
total: number;
}
export interface LLMModel {
name: string;
available: boolean;
provider: LLMModelProvider;
}
export interface LLMModelProvider {
id: string;
providerName: string;
providerType: string;
}
export abstract class LLMApi {
abstract chat(options: ChatOptions): Promise<void>;
abstract usage(): Promise<LLMUsage>;
abstract models(): Promise<LLMModel[]>;
}
type ProviderName = "openai" | "azure" | "claude" | "palm";
@@ -67,8 +94,17 @@ interface ChatProvider {
export class ClientApi {
public llm: LLMApi;
constructor() {
this.llm = new ChatGPTApi();
constructor(provider: ModelProvider = ModelProvider.GPT) {
switch (provider) {
case ModelProvider.GeminiPro:
this.llm = new GeminiProApi();
break;
case ModelProvider.Claude:
this.llm = new ClaudeApi();
break;
default:
this.llm = new ChatGPTApi();
}
}
config() {}
@@ -87,13 +123,13 @@ export class ClientApi {
{
from: "human",
value:
"Share from [ChatGPT Next Web]: https://github.com/Yidadaa/ChatGPT-Next-Web",
"Share from [NextChat]: https://github.com/Yidadaa/ChatGPT-Next-Web",
},
]);
// 敬告二开开发者们,为了开源大模型的发展,请不要修改上述消息,此消息用于后续数据清洗使用
// Please do not modify this message
console.log("[Share]", msgs);
console.log("[Share]", messages, msgs);
const clientConfig = getClientConfig();
const proxyUrl = "/sharegpt";
const rawUrl = "https://sharegpt.com/api/conversations";
@@ -117,28 +153,38 @@ export class ClientApi {
}
}
export const api = new ClientApi();
export function getHeaders() {
const accessStore = useAccessStore.getState();
let headers: Record<string, string> = {
const headers: Record<string, string> = {
"Content-Type": "application/json",
"x-requested-with": "XMLHttpRequest",
Accept: "application/json",
};
const makeBearer = (token: string) => `Bearer ${token.trim()}`;
const modelConfig = useChatStore.getState().currentSession().mask.modelConfig;
const isGoogle = modelConfig.model.startsWith("gemini");
const isAzure = accessStore.provider === ServiceProvider.Azure;
const authHeader = isAzure ? "api-key" : "Authorization";
const apiKey = isGoogle
? accessStore.googleApiKey
: isAzure
? accessStore.azureApiKey
: accessStore.openaiApiKey;
const clientConfig = getClientConfig();
const makeBearer = (s: string) => `${isAzure ? "" : "Bearer "}${s.trim()}`;
const validString = (x: string) => x && x.length > 0;
// use user's api key first
if (validString(accessStore.token)) {
headers.Authorization = makeBearer(accessStore.token);
} else if (
accessStore.enabledAccessControl() &&
validString(accessStore.accessCode)
) {
headers.Authorization = makeBearer(
ACCESS_CODE_PREFIX + accessStore.accessCode,
);
// when using google api in app, not set auth header
if (!(isGoogle && clientConfig?.isApp)) {
// use user's api key first
if (validString(apiKey)) {
headers[authHeader] = makeBearer(apiKey);
} else if (
accessStore.enabledAccessControl() &&
validString(accessStore.accessCode)
) {
headers[authHeader] = makeBearer(
ACCESS_CODE_PREFIX + accessStore.accessCode,
);
}
}
return headers;

View File

@@ -3,17 +3,17 @@ export const ChatControllerPool = {
controllers: {} as Record<string, AbortController>,
addController(
sessionIndex: number,
messageId: number,
sessionId: string,
messageId: string,
controller: AbortController,
) {
const key = this.key(sessionIndex, messageId);
const key = this.key(sessionId, messageId);
this.controllers[key] = controller;
return key;
},
stop(sessionIndex: number, messageId: number) {
const key = this.key(sessionIndex, messageId);
stop(sessionId: string, messageId: string) {
const key = this.key(sessionId, messageId);
const controller = this.controllers[key];
controller?.abort();
},
@@ -26,12 +26,12 @@ export const ChatControllerPool = {
return Object.values(this.controllers).length > 0;
},
remove(sessionIndex: number, messageId: number) {
const key = this.key(sessionIndex, messageId);
remove(sessionId: string, messageId: string) {
const key = this.key(sessionId, messageId);
delete this.controllers[key];
},
key(sessionIndex: number, messageIndex: number) {
return `${sessionIndex},${messageIndex}`;
key(sessionId: string, messageIndex: string) {
return `${sessionId},${messageIndex}`;
},
};

View File

@@ -0,0 +1,408 @@
import { ACCESS_CODE_PREFIX, Anthropic, ApiPath } from "@/app/constant";
import { ChatOptions, LLMApi, MultimodalContent } from "../api";
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
import { getClientConfig } from "@/app/config/client";
import { DEFAULT_API_HOST } from "@/app/constant";
import { RequestMessage } from "@/app/typing";
import {
EventStreamContentType,
fetchEventSource,
} from "@fortaine/fetch-event-source";
import Locale from "../../locales";
import { prettyObject } from "@/app/utils/format";
import { getMessageTextContent, isVisionModel } from "@/app/utils";
export type MultiBlockContent = {
type: "image" | "text";
source?: {
type: string;
media_type: string;
data: string;
};
text?: string;
};
export type AnthropicMessage = {
role: (typeof ClaudeMapper)[keyof typeof ClaudeMapper];
content: string | MultiBlockContent[];
};
export interface AnthropicChatRequest {
model: string; // The model that will complete your prompt.
messages: AnthropicMessage[]; // The prompt that you want Claude to complete.
max_tokens: number; // The maximum number of tokens to generate before stopping.
stop_sequences?: string[]; // Sequences that will cause the model to stop generating completion text.
temperature?: number; // Amount of randomness injected into the response.
top_p?: number; // Use nucleus sampling.
top_k?: number; // Only sample from the top K options for each subsequent token.
metadata?: object; // An object describing metadata about the request.
stream?: boolean; // Whether to incrementally stream the response using server-sent events.
}
export interface ChatRequest {
model: string; // The model that will complete your prompt.
prompt: string; // The prompt that you want Claude to complete.
max_tokens_to_sample: number; // The maximum number of tokens to generate before stopping.
stop_sequences?: string[]; // Sequences that will cause the model to stop generating completion text.
temperature?: number; // Amount of randomness injected into the response.
top_p?: number; // Use nucleus sampling.
top_k?: number; // Only sample from the top K options for each subsequent token.
metadata?: object; // An object describing metadata about the request.
stream?: boolean; // Whether to incrementally stream the response using server-sent events.
}
export interface ChatResponse {
completion: string;
stop_reason: "stop_sequence" | "max_tokens";
model: string;
}
export type ChatStreamResponse = ChatResponse & {
stop?: string;
log_id: string;
};
const ClaudeMapper = {
assistant: "assistant",
user: "user",
system: "user",
} as const;
const keys = ["claude-2, claude-instant-1"];
export class ClaudeApi implements LLMApi {
extractMessage(res: any) {
console.log("[Response] claude response: ", res);
return res?.content?.[0]?.text;
}
async chat(options: ChatOptions): Promise<void> {
const visionModel = isVisionModel(options.config.model);
const accessStore = useAccessStore.getState();
const shouldStream = !!options.config.stream;
const modelConfig = {
...useAppConfig.getState().modelConfig,
...useChatStore.getState().currentSession().mask.modelConfig,
...{
model: options.config.model,
},
};
const messages = [...options.messages];
const keys = ["system", "user"];
// roles must alternate between "user" and "assistant" in claude, so add a fake assistant message between two user messages
for (let i = 0; i < messages.length - 1; i++) {
const message = messages[i];
const nextMessage = messages[i + 1];
if (keys.includes(message.role) && keys.includes(nextMessage.role)) {
messages[i] = [
message,
{
role: "assistant",
content: ";",
},
] as any;
}
}
const prompt = messages
.flat()
.filter((v) => {
if (!v.content) return false;
if (typeof v.content === "string" && !v.content.trim()) return false;
return true;
})
.map((v) => {
const { role, content } = v;
const insideRole = ClaudeMapper[role] ?? "user";
if (!visionModel || typeof content === "string") {
return {
role: insideRole,
content: getMessageTextContent(v),
};
}
return {
role: insideRole,
content: content
.filter((v) => v.image_url || v.text)
.map(({ type, text, image_url }) => {
if (type === "text") {
return {
type,
text: text!,
};
}
const { url = "" } = image_url || {};
const colonIndex = url.indexOf(":");
const semicolonIndex = url.indexOf(";");
const comma = url.indexOf(",");
const mimeType = url.slice(colonIndex + 1, semicolonIndex);
const encodeType = url.slice(semicolonIndex + 1, comma);
const data = url.slice(comma + 1);
return {
type: "image" as const,
source: {
type: encodeType,
media_type: mimeType,
data,
},
};
}),
};
});
const requestBody: AnthropicChatRequest = {
messages: prompt,
stream: shouldStream,
model: modelConfig.model,
max_tokens: modelConfig.max_tokens,
temperature: modelConfig.temperature,
top_p: modelConfig.top_p,
// top_k: modelConfig.top_k,
top_k: 5,
};
const path = this.path(Anthropic.ChatPath);
const controller = new AbortController();
options.onController?.(controller);
const payload = {
method: "POST",
body: JSON.stringify(requestBody),
signal: controller.signal,
headers: {
"Content-Type": "application/json",
Accept: "application/json",
"x-api-key": accessStore.anthropicApiKey,
"anthropic-version": accessStore.anthropicApiVersion,
Authorization: getAuthKey(accessStore.anthropicApiKey),
},
};
if (shouldStream) {
try {
const context = {
text: "",
finished: false,
};
const finish = () => {
if (!context.finished) {
options.onFinish(context.text);
context.finished = true;
}
};
controller.signal.onabort = finish;
fetchEventSource(path, {
...payload,
async onopen(res) {
const contentType = res.headers.get("content-type");
console.log("response content type: ", contentType);
if (contentType?.startsWith("text/plain")) {
context.text = await res.clone().text();
return finish();
}
if (
!res.ok ||
!res.headers
.get("content-type")
?.startsWith(EventStreamContentType) ||
res.status !== 200
) {
const responseTexts = [context.text];
let extraInfo = await res.clone().text();
try {
const resJson = await res.clone().json();
extraInfo = prettyObject(resJson);
} catch {}
if (res.status === 401) {
responseTexts.push(Locale.Error.Unauthorized);
}
if (extraInfo) {
responseTexts.push(extraInfo);
}
context.text = responseTexts.join("\n\n");
return finish();
}
},
onmessage(msg) {
let chunkJson:
| undefined
| {
type: "content_block_delta" | "content_block_stop";
delta?: {
type: "text_delta";
text: string;
};
index: number;
};
try {
chunkJson = JSON.parse(msg.data);
} catch (e) {
console.error("[Response] parse error", msg.data);
}
if (!chunkJson || chunkJson.type === "content_block_stop") {
return finish();
}
const { delta } = chunkJson;
if (delta?.text) {
context.text += delta.text;
options.onUpdate?.(context.text, delta.text);
}
},
onclose() {
finish();
},
onerror(e) {
options.onError?.(e);
throw e;
},
openWhenHidden: true,
});
} catch (e) {
console.error("failed to chat", e);
options.onError?.(e as Error);
}
} else {
try {
controller.signal.onabort = () => options.onFinish("");
const res = await fetch(path, payload);
const resJson = await res.json();
const message = this.extractMessage(resJson);
options.onFinish(message);
} catch (e) {
console.error("failed to chat", e);
options.onError?.(e as Error);
}
}
}
async usage() {
return {
used: 0,
total: 0,
};
}
async models() {
// const provider = {
// id: "anthropic",
// providerName: "Anthropic",
// providerType: "anthropic",
// };
return [
// {
// name: "claude-instant-1.2",
// available: true,
// provider,
// },
// {
// name: "claude-2.0",
// available: true,
// provider,
// },
// {
// name: "claude-2.1",
// available: true,
// provider,
// },
// {
// name: "claude-3-opus-20240229",
// available: true,
// provider,
// },
// {
// name: "claude-3-sonnet-20240229",
// available: true,
// provider,
// },
// {
// name: "claude-3-haiku-20240307",
// available: true,
// provider,
// },
];
}
path(path: string): string {
const accessStore = useAccessStore.getState();
let baseUrl: string = "";
if (accessStore.useCustomConfig) {
baseUrl = accessStore.anthropicUrl;
}
// if endpoint is empty, use default endpoint
if (baseUrl.trim().length === 0) {
const isApp = !!getClientConfig()?.isApp;
baseUrl = isApp
? DEFAULT_API_HOST + "/api/proxy/anthropic"
: ApiPath.Anthropic;
}
if (!baseUrl.startsWith("http") && !baseUrl.startsWith("/api")) {
baseUrl = "https://" + baseUrl;
}
baseUrl = trimEnd(baseUrl, "/");
return `${baseUrl}/${path}`;
}
}
function trimEnd(s: string, end = " ") {
if (end.length === 0) return s;
while (s.endsWith(end)) {
s = s.slice(0, -end.length);
}
return s;
}
function bearer(value: string) {
return `Bearer ${value.trim()}`;
}
function getAuthKey(apiKey = "") {
const accessStore = useAccessStore.getState();
const isApp = !!getClientConfig()?.isApp;
let authKey = "";
if (apiKey) {
// use user's api key first
authKey = bearer(apiKey);
} else if (
accessStore.enabledAccessControl() &&
!isApp &&
!!accessStore.accessCode
) {
// or use access code
authKey = bearer(ACCESS_CODE_PREFIX + accessStore.accessCode);
}
return authKey;
}

View File

@@ -0,0 +1,284 @@
import { 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 {
getMessageTextContent,
getMessageImages,
isVisionModel,
} from "@/app/utils";
export class GeminiProApi implements LLMApi {
extractMessage(res: any) {
console.log("[Response] gemini-pro response: ", res);
return (
res?.candidates?.at(0)?.content?.parts.at(0)?.text ||
res?.error?.message ||
""
);
}
async chat(options: ChatOptions): Promise<void> {
// const apiClient = this;
const visionModel = isVisionModel(options.config.model);
let multimodal = false;
const messages = options.messages.map((v) => {
let parts: any[] = [{ text: getMessageTextContent(v) }];
if (visionModel) {
const images = getMessageImages(v);
if (images.length > 0) {
multimodal = true;
parts = parts.concat(
images.map((image) => {
const imageType = image.split(";")[0].split(":")[1];
const imageData = image.split(",")[1];
return {
inline_data: {
mime_type: imageType,
data: imageData,
},
};
}),
);
}
}
return {
role: v.role.replace("assistant", "model").replace("system", "user"),
parts: parts,
};
});
// google requires that role in neighboring messages must not be the same
for (let i = 0; i < messages.length - 1; ) {
// Check if current and next item both have the role "model"
if (messages[i].role === messages[i + 1].role) {
// Concatenate the 'parts' of the current and next item
messages[i].parts = messages[i].parts.concat(messages[i + 1].parts);
// Remove the next item
messages.splice(i + 1, 1);
} else {
// Move to the next item
i++;
}
}
// if (visionModel && messages.length > 1) {
// options.onError?.(new Error("Multiturn chat is not enabled for models/gemini-pro-vision"));
// }
const modelConfig = {
...useAppConfig.getState().modelConfig,
...useChatStore.getState().currentSession().mask.modelConfig,
...{
model: options.config.model,
},
};
const requestPayload = {
contents: messages,
generationConfig: {
// stopSequences: [
// "Title"
// ],
temperature: modelConfig.temperature,
maxOutputTokens: modelConfig.max_tokens,
topP: modelConfig.top_p,
// "topK": modelConfig.top_k,
},
safetySettings: [
{
category: "HARM_CATEGORY_HARASSMENT",
threshold: "BLOCK_ONLY_HIGH",
},
{
category: "HARM_CATEGORY_HATE_SPEECH",
threshold: "BLOCK_ONLY_HIGH",
},
{
category: "HARM_CATEGORY_SEXUALLY_EXPLICIT",
threshold: "BLOCK_ONLY_HIGH",
},
{
category: "HARM_CATEGORY_DANGEROUS_CONTENT",
threshold: "BLOCK_ONLY_HIGH",
},
],
};
const accessStore = useAccessStore.getState();
let baseUrl = "";
if (accessStore.useCustomConfig) {
baseUrl = accessStore.googleUrl;
}
const isApp = !!getClientConfig()?.isApp;
let shouldStream = !!options.config.stream;
const controller = new AbortController();
options.onController?.(controller);
try {
let googleChatPath = visionModel
? Google.VisionChatPath(modelConfig.model)
: Google.ChatPath(modelConfig.model);
let chatPath = this.path(googleChatPath);
// let baseUrl = accessStore.googleUrl;
if (!baseUrl) {
baseUrl = isApp
? DEFAULT_API_HOST + "/api/proxy/google/" + googleChatPath
: chatPath;
}
if (isApp) {
baseUrl += `?key=${accessStore.googleApiKey}`;
}
const chatPayload = {
method: "POST",
body: JSON.stringify(requestPayload),
signal: controller.signal,
headers: getHeaders(),
};
// make a fetch request
const requestTimeoutId = setTimeout(
() => controller.abort(),
REQUEST_TIMEOUT_MS,
);
if (shouldStream) {
let responseText = "";
let remainText = "";
let finished = false;
let existingTexts: string[] = [];
const finish = () => {
finished = true;
options.onFinish(existingTexts.join(""));
};
// 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();
fetch(
baseUrl.replace("generateContent", "streamGenerateContent"),
chatPayload,
)
.then((response) => {
const reader = response?.body?.getReader();
const decoder = new TextDecoder();
let partialData = "";
return reader?.read().then(function processText({
done,
value,
}): Promise<any> {
if (done) {
if (response.status !== 200) {
try {
let data = JSON.parse(ensureProperEnding(partialData));
if (data && data[0].error) {
options.onError?.(new Error(data[0].error.message));
} else {
options.onError?.(new Error("Request failed"));
}
} catch (_) {
options.onError?.(new Error("Request failed"));
}
}
console.log("Stream complete");
// options.onFinish(responseText + remainText);
finished = true;
return Promise.resolve();
}
partialData += decoder.decode(value, { stream: true });
try {
let data = JSON.parse(ensureProperEnding(partialData));
const textArray = data.reduce(
(acc: string[], item: { candidates: any[] }) => {
const texts = item.candidates.map((candidate) =>
candidate.content.parts
.map((part: { text: any }) => part.text)
.join(""),
);
return acc.concat(texts);
},
[],
);
if (textArray.length > existingTexts.length) {
const deltaArray = textArray.slice(existingTexts.length);
existingTexts = textArray;
remainText += deltaArray.join("");
}
} catch (error) {
// console.log("[Response Animation] error: ", error,partialData);
// skip error message when parsing json
}
return reader.read().then(processText);
});
})
.catch((error) => {
console.error("Error:", error);
});
} else {
const res = await fetch(baseUrl, chatPayload);
clearTimeout(requestTimeoutId);
const resJson = await res.json();
if (resJson?.promptFeedback?.blockReason) {
// being blocked
options.onError?.(
new Error(
"Message is being blocked for reason: " +
resJson.promptFeedback.blockReason,
),
);
}
const message = this.extractMessage(resJson);
options.onFinish(message);
}
} catch (e) {
console.log("[Request] failed to make a chat request", e);
options.onError?.(e as Error);
}
}
usage(): Promise<LLMUsage> {
throw new Error("Method not implemented.");
}
async models(): Promise<LLMModel[]> {
return [];
}
path(path: string): string {
return "/api/google/" + path;
}
}
function ensureProperEnding(str: string) {
if (str.startsWith("[") && !str.endsWith("]")) {
return str + "]";
}
return str;
}

View File

@@ -1,21 +1,100 @@
import { OpenaiPath, REQUEST_TIMEOUT_MS } from "@/app/constant";
"use client";
import {
ApiPath,
DEFAULT_API_HOST,
DEFAULT_MODELS,
OpenaiPath,
REQUEST_TIMEOUT_MS,
ServiceProvider,
} from "@/app/constant";
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
import { ChatOptions, getHeaders, LLMApi, LLMUsage } from "../api";
import {
ChatOptions,
getHeaders,
LLMApi,
LLMModel,
LLMUsage,
MultimodalContent,
} from "../api";
import Locale from "../../locales";
import {
EventStreamContentType,
fetchEventSource,
} from "@fortaine/fetch-event-source";
import { prettyObject } from "@/app/utils/format";
import { getClientConfig } from "@/app/config/client";
import { makeAzurePath } from "@/app/azure";
import {
getMessageTextContent,
getMessageImages,
isVisionModel,
} from "@/app/utils";
export interface OpenAIListModelResponse {
object: string;
data: Array<{
id: string;
object: string;
root: string;
}>;
}
interface RequestPayload {
messages: {
role: "system" | "user" | "assistant";
content: string | MultimodalContent[];
}[];
stream?: boolean;
model: string;
temperature: number;
presence_penalty: number;
frequency_penalty: number;
top_p: number;
max_tokens?: number;
}
export class ChatGPTApi implements LLMApi {
private disableListModels = true;
path(path: string): string {
let openaiUrl = useAccessStore.getState().openaiUrl;
if (openaiUrl.endsWith("/")) {
openaiUrl = openaiUrl.slice(0, openaiUrl.length - 1);
const accessStore = useAccessStore.getState();
let baseUrl = "";
if (accessStore.useCustomConfig) {
const isAzure = accessStore.provider === ServiceProvider.Azure;
if (isAzure && !accessStore.isValidAzure()) {
throw Error(
"incomplete azure config, please check it in your settings page",
);
}
if (isAzure) {
path = makeAzurePath(path, accessStore.azureApiVersion);
}
baseUrl = isAzure ? accessStore.azureUrl : accessStore.openaiUrl;
}
return [openaiUrl, path].join("/");
if (baseUrl.length === 0) {
const isApp = !!getClientConfig()?.isApp;
baseUrl = isApp
? DEFAULT_API_HOST + "/proxy" + ApiPath.OpenAI
: ApiPath.OpenAI;
}
if (baseUrl.endsWith("/")) {
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
}
if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.OpenAI)) {
baseUrl = "https://" + baseUrl;
}
console.log("[Proxy Endpoint] ", baseUrl, path);
return [baseUrl, path].join("/");
}
extractMessage(res: any) {
@@ -23,9 +102,10 @@ export class ChatGPTApi implements LLMApi {
}
async chat(options: ChatOptions) {
const visionModel = isVisionModel(options.config.model);
const messages = options.messages.map((v) => ({
role: v.role,
content: v.content,
content: visionModel ? v.content : getMessageTextContent(v),
}));
const modelConfig = {
@@ -36,14 +116,23 @@ export class ChatGPTApi implements LLMApi {
},
};
const requestPayload = {
const requestPayload: RequestPayload = {
messages,
stream: options.config.stream,
model: modelConfig.model,
temperature: modelConfig.temperature,
presence_penalty: modelConfig.presence_penalty,
frequency_penalty: modelConfig.frequency_penalty,
top_p: modelConfig.top_p,
// max_tokens: Math.max(modelConfig.max_tokens, 1024),
// Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore.
};
// add max_tokens to vision model
if (visionModel) {
requestPayload["max_tokens"] = Math.max(modelConfig.max_tokens, 4000);
}
console.log("[Request] openai payload: ", requestPayload);
const shouldStream = !!options.config.stream;
@@ -67,12 +156,38 @@ export class ChatGPTApi implements LLMApi {
if (shouldStream) {
let responseText = "";
let remainText = "";
let finished = false;
// animate response to make it looks smooth
function animateResponseText() {
if (finished || controller.signal.aborted) {
responseText += remainText;
console.log("[Response Animation] finished");
if (responseText?.length === 0) {
options.onError?.(new Error("empty response from server"));
}
return;
}
if (remainText.length > 0) {
const fetchCount = Math.max(1, Math.round(remainText.length / 60));
const fetchText = remainText.slice(0, fetchCount);
responseText += fetchText;
remainText = remainText.slice(fetchCount);
options.onUpdate?.(responseText, fetchText);
}
requestAnimationFrame(animateResponseText);
}
// start animaion
animateResponseText();
const finish = () => {
if (!finished) {
options.onFinish(responseText);
finished = true;
options.onFinish(responseText + remainText);
}
};
@@ -127,10 +242,27 @@ export class ChatGPTApi implements LLMApi {
const text = msg.data;
try {
const json = JSON.parse(text);
const delta = json.choices[0].delta.content;
const choices = json.choices as Array<{
delta: { content: string };
}>;
const delta = choices[0]?.delta?.content;
const textmoderation = json?.prompt_filter_results;
if (delta) {
responseText += delta;
options.onUpdate?.(responseText, delta);
remainText += delta;
}
if (
textmoderation &&
textmoderation.length > 0 &&
ServiceProvider.Azure
) {
const contentFilterResults =
textmoderation[0]?.content_filter_results;
console.log(
`[${ServiceProvider.Azure}] [Text Moderation] flagged categories result:`,
contentFilterResults,
);
}
} catch (e) {
console.error("[Request] parse error", text, msg);
@@ -154,7 +286,7 @@ export class ChatGPTApi implements LLMApi {
options.onFinish(message);
}
} catch (e) {
console.log("[Request] failed to make a chat reqeust", e);
console.log("[Request] failed to make a chat request", e);
options.onError?.(e as Error);
}
}
@@ -223,5 +355,36 @@ export class ChatGPTApi implements LLMApi {
total: total.hard_limit_usd,
} as LLMUsage;
}
async models(): Promise<LLMModel[]> {
if (this.disableListModels) {
return DEFAULT_MODELS.slice();
}
const res = await fetch(this.path(OpenaiPath.ListModelPath), {
method: "GET",
headers: {
...getHeaders(),
},
});
const resJson = (await res.json()) as OpenAIListModelResponse;
const chatModels = resJson.data?.filter((m) => m.id.startsWith("gpt-"));
console.log("[Models]", chatModels);
if (!chatModels) {
return [];
}
return chatModels.map((m) => ({
name: m.id,
available: true,
provider: {
id: "openai",
providerName: "OpenAI",
providerType: "openai",
},
}));
}
}
export { OpenaiPath };

View File

@@ -1,28 +1,77 @@
import { useSearchParams } from "react-router-dom";
import { useEffect } from "react";
// import { useSearchParams } from "react-router-dom";
import { useSearchParams } from "next/navigation";
import Locale from "./locales";
type Command = (param: string) => void;
interface Commands {
fill?: Command;
submit?: Command;
mask?: Command;
code?: Command;
settings?: Command;
}
export function useCommand(commands: Commands = {}) {
const [searchParams, setSearchParams] = useSearchParams();
if (commands === undefined) return;
// fixme: update commands
// useEffect(() => {
// let shouldUpdate = false;
// searchParams.forEach((param, name) => {
// const commandName = name as keyof Commands;
// if (typeof commands[commandName] === "function") {
// commands[commandName]!(param);
// searchParams.delete(name);
// shouldUpdate = true;
// }
// });
let shouldUpdate = false;
searchParams.forEach((param, name) => {
const commandName = name as keyof Commands;
if (typeof commands[commandName] === "function") {
commands[commandName]!(param);
searchParams.delete(name);
shouldUpdate = true;
}
});
if (shouldUpdate) {
setSearchParams(searchParams);
}
// if (shouldUpdate) {
// setSearchParams(searchParams);
// }
// // eslint-disable-next-line react-hooks/exhaustive-deps
// }, [searchParams, commands]);
}
interface ChatCommands {
new?: Command;
newm?: Command;
next?: Command;
prev?: Command;
clear?: Command;
del?: Command;
}
export const ChatCommandPrefix = ":";
export function useChatCommand(commands: ChatCommands = {}) {
function extract(userInput: string) {
return (
userInput.startsWith(ChatCommandPrefix) ? userInput.slice(1) : userInput
) as keyof ChatCommands;
}
function search(userInput: string) {
const input = extract(userInput);
const desc = Locale.Chat.Commands;
return Object.keys(commands)
.filter((c) => c.startsWith(input))
.map((c) => ({
title: desc[c as keyof ChatCommands],
content: ChatCommandPrefix + c,
}));
}
function match(userInput: string) {
const command = extract(userInput);
const matched = typeof commands[command] === "function";
return {
matched,
invoke: () => matched && commands[command]!(userInput),
};
}
return { match, search };
}

View File

@@ -0,0 +1,123 @@
import { isValidElement } from "react";
type IconMap = {
active?: JSX.Element;
inactive?: JSX.Element;
mobileActive?: JSX.Element;
mobileInactive?: JSX.Element;
};
interface Action {
id: string;
title?: string;
icons: JSX.Element | IconMap;
className?: string;
onClick?: () => void;
activeClassName?: string;
}
type Groups = {
normal: string[][];
mobile: string[][];
};
export interface ActionsBarProps {
actionsShema: Action[];
onSelect?: (id: string) => void;
selected?: string;
groups: string[][] | Groups;
className?: string;
inMobile?: boolean;
}
export default function ActionsBar(props: ActionsBarProps) {
const { actionsShema, onSelect, selected, groups, className, inMobile } =
props;
const handlerClick =
(action: Action) => (e: { preventDefault: () => void }) => {
e.preventDefault();
if (action.onClick) {
action.onClick();
}
if (selected !== action.id) {
onSelect?.(action.id);
}
};
const internalGroup = Array.isArray(groups)
? groups
: inMobile
? groups.mobile
: groups.normal;
const content = internalGroup.reduce((res, group, ind, arr) => {
res.push(
...group.map((i) => {
const action = actionsShema.find((a) => a.id === i);
if (!action) {
return <></>;
}
const { icons } = action;
let activeIcon, inactiveIcon, mobileActiveIcon, mobileInactiveIcon;
if (isValidElement(icons)) {
activeIcon = icons;
inactiveIcon = icons;
mobileActiveIcon = icons;
mobileInactiveIcon = icons;
} else {
activeIcon = (icons as IconMap).active;
inactiveIcon = (icons as IconMap).inactive;
mobileActiveIcon = (icons as IconMap).mobileActive;
mobileInactiveIcon = (icons as IconMap).mobileInactive;
}
if (inMobile) {
return (
<div
key={action.id}
className={` cursor-pointer shrink-1 grow-0 basis-[${
(100 - 1) / arr.length
}%] flex flex-col items-center justify-around gap-0.5 py-1.5
${
selected === action.id
? "text-text-sidebar-tab-mobile-active"
: "text-text-sidebar-tab-mobile-inactive"
}
`}
onClick={handlerClick(action)}
>
{selected === action.id ? mobileActiveIcon : mobileInactiveIcon}
<div className=" leading-3 text-sm-mobile-tab h-3 font-common w-[100%]">
{action.title || " "}
</div>
</div>
);
}
return (
<div
key={action.id}
className={`cursor-pointer p-3 ${
selected === action.id
? `!bg-actions-bar-btn-default ${action.activeClassName}`
: "bg-transparent"
} rounded-md items-center ${
action.className
} transition duration-300 ease-in-out`}
onClick={handlerClick(action)}
>
{selected === action.id ? activeIcon : inactiveIcon}
</div>
);
}),
);
if (ind < arr.length - 1) {
res.push(<div key={String(ind)} className=" flex-1"></div>);
}
return res;
}, [] as JSX.Element[]);
return <div className={`flex items-center ${className} `}>{content}</div>;
}

View File

@@ -0,0 +1,78 @@
import * as React from "react";
export type ButtonType = "primary" | "danger" | null;
export interface BtnProps {
onClick?: () => void;
icon?: JSX.Element;
prefixIcon?: JSX.Element;
type?: ButtonType;
text?: React.ReactNode;
bordered?: boolean;
shadow?: boolean;
className?: string;
title?: string;
disabled?: boolean;
tabIndex?: number;
autoFocus?: boolean;
}
export default function Btn(props: BtnProps) {
const {
onClick,
icon,
type,
text,
className,
title,
disabled,
tabIndex,
autoFocus,
prefixIcon,
} = props;
let btnClassName;
switch (type) {
case "primary":
btnClassName = `${
disabled
? "bg-primary-btn-disabled dark:opacity-30 dark:text-primary-btn-disabled-dark"
: "bg-primary-btn shadow-btn"
} text-text-btn-primary `;
break;
case "danger":
btnClassName = `bg-danger-btn text-text-btn-danger hover:bg-hovered-danger-btn`;
break;
default:
btnClassName = `bg-default-btn text-text-btn-default hover:bg-hovered-btn`;
}
return (
<button
className={`
${className ?? ""}
py-2 px-3 flex items-center justify-center gap-1 rounded-action-btn transition-all duration-300 select-none
${disabled ? "cursor-not-allowed" : "cursor-pointer"}
${btnClassName}
follow-parent-svg
`}
onClick={onClick}
title={title}
disabled={disabled}
role="button"
tabIndex={tabIndex}
autoFocus={autoFocus}
>
{prefixIcon && (
<div className={`flex items-center justify-center`}>{prefixIcon}</div>
)}
{text && (
<div className={`font-common text-sm-title leading-4 line-clamp-1`}>
{text}
</div>
)}
{icon && <div className={`flex items-center justify-center`}>{icon}</div>}
</button>
);
}

View File

@@ -0,0 +1,32 @@
import { ReactNode } from "react";
export interface CardProps {
className?: string;
children?: ReactNode;
title?: ReactNode;
}
export default function Card(props: CardProps) {
const { className, children, title } = props;
return (
<>
{title && (
<div
className={`
capitalize !font-semibold text-sm-mobile font-weight-setting-card-title text-text-card-title
mb-3
ml-3
md:ml-4
`}
>
{title}
</div>
)}
<div className={`px-4 py-1 rounded-lg bg-card ${className}`}>
{children}
</div>
</>
);
}

View File

@@ -0,0 +1,18 @@
import BotIcon from "@/app/icons/bot.svg";
import LoadingIcon from "@/app/icons/three-dots.svg";
export default function GloablLoading({
noLogo,
}: {
noLogo?: boolean;
useSkeleton?: boolean;
}) {
return (
<div
className={`flex flex-col justify-center items-center w-[100%] h-[100%]`}
>
{!noLogo && <BotIcon />}
<LoadingIcon />
</div>
);
}

View File

@@ -0,0 +1,39 @@
import * as HoverCard from "@radix-ui/react-hover-card";
import { ComponentProps } from "react";
export interface PopoverProps {
content?: JSX.Element | string;
children?: JSX.Element;
arrowClassName?: string;
popoverClassName?: string;
noArrow?: boolean;
align?: ComponentProps<typeof HoverCard.Content>["align"];
openDelay?: number;
}
export default function HoverPopover(props: PopoverProps) {
const {
content,
children,
arrowClassName,
popoverClassName,
noArrow = false,
align,
openDelay = 300,
} = props;
return (
<HoverCard.Root openDelay={openDelay}>
<HoverCard.Trigger asChild>{children}</HoverCard.Trigger>
<HoverCard.Portal>
<HoverCard.Content
className={`${popoverClassName}`}
sideOffset={5}
align={align}
>
{content}
{!noArrow && <HoverCard.Arrow className={`${arrowClassName}`} />}
</HoverCard.Content>
</HoverCard.Portal>
</HoverCard.Root>
);
}

View File

@@ -0,0 +1,42 @@
import { CSSProperties } from "react";
import { getMessageImages } from "@/app/utils";
import { RequestMessage } from "@/app/client/api";
interface ImgsProps {
message: RequestMessage;
}
export default function Imgs(props: ImgsProps) {
const { message } = props;
const imgSrcs = getMessageImages(message);
if (imgSrcs.length < 1) {
return <></>;
}
const imgVars = {
"--imgs-width": `calc(var(--max-message-width) - ${
imgSrcs.length - 1
}*0.25rem)`,
"--img-width": `calc(var(--imgs-width)/ ${imgSrcs.length})`,
};
return (
<div
className={`w-[100%] mt-[0.625rem] flex gap-1`}
style={imgVars as CSSProperties}
>
{imgSrcs.map((image, index) => {
return (
<div
key={index}
className="flex-1 min-w-[var(--img-width)] pb-[var(--img-width)] object-cover bg-cover bg-no-repeat bg-center box-border rounded-chat-img"
style={{
backgroundImage: `url(${image})`,
}}
/>
);
})}
</div>
);
}

View File

@@ -0,0 +1,88 @@
import PasswordVisible from "@/app/icons/passwordVisible.svg";
import PasswordInvisible from "@/app/icons/passwordInvisible.svg";
import {
DetailedHTMLProps,
InputHTMLAttributes,
useContext,
useLayoutEffect,
useState,
} from "react";
import List, { ListContext } from "@/app/components/List";
export interface CommonInputProps
extends Omit<
DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>,
"onChange" | "type" | "value"
> {
className?: string;
}
export interface NumberInputProps {
onChange?: (v: number) => void;
type?: "number";
value?: number;
}
export interface TextInputProps {
onChange?: (v: string) => void;
type?: "text" | "password";
value?: string;
}
export interface InputProps {
onChange?: ((v: string) => void) | ((v: number) => void);
type?: "text" | "password" | "number";
value?: string | number;
}
export default function Input(
props: CommonInputProps & NumberInputProps,
): JSX.Element;
export default function Input(
props: CommonInputProps & TextInputProps,
): JSX.Element;
export default function Input(props: CommonInputProps & InputProps) {
const { value, type = "text", onChange, className, ...rest } = props;
const [show, setShow] = useState(false);
const { inputClassName } = useContext(ListContext);
const internalType = (show && "text") || type;
const { update, handleValidate } = useContext(List.ListContext);
useLayoutEffect(() => {
update?.({ type: "input" });
}, []);
useLayoutEffect(() => {
handleValidate?.(value);
}, [value]);
return (
<div
className={` group/input w-[100%] rounded-chat-input bg-input transition-colors duration-300 ease-in-out flex gap-3 items-center px-3 py-2 ${className} hover:bg-select-hover ${inputClassName}`}
>
<input
{...rest}
className=" overflow-hidden text-text-input text-sm-title leading-input outline-none flex-1 group-hover/input:bg-input-input-ele-hover"
type={internalType}
value={value}
onChange={(e) => {
if (type === "number") {
const v = e.currentTarget.valueAsNumber;
(onChange as NumberInputProps["onChange"])?.(v);
} else {
const v = e.currentTarget.value;
(onChange as TextInputProps["onChange"])?.(v);
}
}}
/>
{type == "password" && (
<div className=" cursor-pointer" onClick={() => setShow((pre) => !pre)}>
{show ? <PasswordVisible /> : <PasswordInvisible />}
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,157 @@
import {
ReactNode,
createContext,
useCallback,
useContext,
useState,
} from "react";
interface WidgetStyle {
selectClassName?: string;
inputClassName?: string;
rangeClassName?: string;
switchClassName?: string;
inputNextLine?: boolean;
rangeNextLine?: boolean;
}
interface ChildrenMeta {
type?: "unknown" | "input" | "range";
error?: string;
}
export interface ListProps {
className?: string;
children?: ReactNode;
id?: string;
isMobileScreen?: boolean;
widgetStyle?: WidgetStyle;
}
type Error =
| {
error: true;
message: string;
}
| {
error: false;
};
export interface ListItemProps {
title: string;
subTitle?: string;
children?: JSX.Element | JSX.Element[];
className?: string;
onClick?: () => void;
nextline?: boolean;
validator?: (v: any) => Error | Promise<Error>;
}
export const ListContext = createContext<
{
isMobileScreen?: boolean;
update?: (m: ChildrenMeta) => void;
handleValidate?: (v: any) => void;
} & WidgetStyle
>({ isMobileScreen: false });
export function ListItem(props: ListItemProps) {
const {
className = "",
onClick,
title,
subTitle,
children,
nextline,
validator,
} = props;
const context = useContext(ListContext);
const [childrenMeta, setMeta] = useState<ChildrenMeta>({});
const { inputNextLine, rangeNextLine } = context;
const { type, error } = childrenMeta;
let internalNextLine;
switch (type) {
case "input":
internalNextLine = !!(nextline || inputNextLine);
break;
case "range":
internalNextLine = !!(nextline || rangeNextLine);
break;
default:
internalNextLine = false;
}
const update = useCallback((m: ChildrenMeta) => {
setMeta((pre) => ({ ...pre, ...m }));
}, []);
const handleValidate = useCallback((v: any) => {
const insideValidator = validator || (() => {});
Promise.resolve(insideValidator(v)).then((result) => {
if (result && result.error) {
return update({
error: result.message,
});
}
update({
error: undefined,
});
});
}, []);
return (
<div
className={`relative after:h-[0.5px] after:bottom-0 after:w-[100%] after:left-0 after:absolute last:after:hidden after:bg-list-item-divider ${
internalNextLine ? "" : "flex gap-3"
} justify-between items-center px-0 py-2 md:py-3 ${className}`}
onClick={onClick}
>
<div className={`flex-1 flex flex-col justify-start gap-1`}>
<div className=" font-common text-sm-mobile font-weight-[500] line-clamp-1 text-text-list-title">
{title}
</div>
{subTitle && (
<div className={` text-sm text-text-list-subtitle`}>{subTitle}</div>
)}
</div>
<ListContext.Provider value={{ ...context, update, handleValidate }}>
<div
className={`${
internalNextLine ? "mt-[0.625rem]" : "max-w-[70%]"
} flex flex-col items-center justify-center`}
>
<div>{children}</div>
{!!error && (
<div className="text-text-btn-danger text-sm-mobile-tab mt-[0.3125rem] flex items-start w-[100%]">
<div className="">{error}</div>
</div>
)}
</div>
</ListContext.Provider>
</div>
);
}
function List(props: ListProps) {
const { className, children, id, widgetStyle } = props;
const { isMobileScreen } = useContext(ListContext);
return (
<ListContext.Provider value={{ isMobileScreen, ...widgetStyle }}>
<div className={`flex flex-col w-[100%] ${className}`} id={id}>
{children}
</div>
</ListContext.Provider>
);
}
List.ListItem = ListItem;
List.ListContext = ListContext;
export default List;

View File

@@ -0,0 +1,35 @@
import BotIcon from "@/app/icons/bot.svg";
import LoadingIcon from "@/app/icons/three-dots.svg";
import { getCSSVar } from "@/app/utils";
export default function Loading({
noLogo,
useSkeleton = true,
}: {
noLogo?: boolean;
useSkeleton?: boolean;
}) {
let theme;
if (typeof window !== "undefined") {
theme = getCSSVar("--default-container-bg");
}
return (
<div
className={`
flex flex-col justify-center items-center w-[100%]
h-[100%]
md:my-2.5
md:ml-1
md:mr-2.5
md:rounded-md
md:h-[calc(100%-1.25rem)]
`}
style={{ background: useSkeleton ? theme : "" }}
>
{!noLogo && <BotIcon />}
<LoadingIcon />
</div>
);
}

View File

@@ -0,0 +1,115 @@
import {
DEFAULT_SIDEBAR_WIDTH,
MAX_SIDEBAR_WIDTH,
MIN_SIDEBAR_WIDTH,
Path,
} from "@/app/constant";
import useDrag from "@/app/hooks/useDrag";
import useMobileScreen from "@/app/hooks/useMobileScreen";
import { updateGlobalCSSVars } from "@/app/utils/client";
import { ComponentType, useRef, useState } from "react";
import { useAppConfig } from "@/app/store/config";
export interface MenuWrapperInspectProps {
setExternalProps?: (v: Record<string, any>) => void;
setShowPanel?: (v: boolean) => void;
showPanel?: boolean;
[k: string]: any;
}
export default function MenuLayout<
ListComponentProps extends MenuWrapperInspectProps,
PanelComponentProps extends MenuWrapperInspectProps,
>(
ListComponent: ComponentType<ListComponentProps>,
PanelComponent: ComponentType<PanelComponentProps>,
) {
return function MenuHood(props: ListComponentProps & PanelComponentProps) {
const [showPanel, setShowPanel] = useState(false);
const [externalProps, setExternalProps] = useState({});
const config = useAppConfig();
const isMobileScreen = useMobileScreen();
const startDragWidth = useRef(config.sidebarWidth ?? DEFAULT_SIDEBAR_WIDTH);
// drag side bar
const { onDragStart } = useDrag({
customToggle: () => {
config.update((config) => {
config.sidebarWidth = DEFAULT_SIDEBAR_WIDTH;
});
},
customDragMove: (nextWidth: number) => {
const { menuWidth } = updateGlobalCSSVars(nextWidth);
document.documentElement.style.setProperty(
"--menu-width",
`${menuWidth}px`,
);
config.update((config) => {
config.sidebarWidth = nextWidth;
});
},
customLimit: (x: number) =>
Math.max(
MIN_SIDEBAR_WIDTH,
Math.min(MAX_SIDEBAR_WIDTH, startDragWidth.current + x),
),
});
return (
<div
className={`
w-[100%] relative bg-center
max-md:h-[100%]
md:flex md:my-2.5
`}
>
<div
className={`
flex flex-col px-6
h-[100%]
max-md:w-[100%] max-md:px-4 max-md:pb-4 max-md:flex-1
md:relative md:basis-sidebar md:pb-6 md:rounded-md md:bg-menu
`}
>
<ListComponent
{...props}
setShowPanel={setShowPanel}
setExternalProps={setExternalProps}
showPanel={showPanel}
/>
</div>
{!isMobileScreen && (
<div
className={`group/menu-dragger cursor-col-resize w-[0.25rem] flex items-center justify-center`}
onPointerDown={(e) => {
startDragWidth.current = config.sidebarWidth;
onDragStart(e as any);
}}
>
<div className="w-[2px] opacity-0 group-hover/menu-dragger:opacity-100 bg-menu-dragger h-[100%] rounded-[2px]">
&nbsp;
</div>
</div>
)}
<div
className={`
md:flex-1 md:h-[100%] md:w-page
max-md:transition-all max-md:duration-300 max-md:absolute max-md:top-0 max-md:max-h-[100vh] max-md:w-[100%] ${
showPanel ? "max-md:left-0" : "max-md:left-[101%]"
} max-md:z-10
`}
>
<PanelComponent
{...props}
{...externalProps}
setShowPanel={setShowPanel}
setExternalProps={setExternalProps}
showPanel={showPanel}
/>
</div>
</div>
);
};
}

View File

@@ -0,0 +1,359 @@
import React, { useLayoutEffect, useState } from "react";
import { createRoot } from "react-dom/client";
import * as AlertDialog from "@radix-ui/react-alert-dialog";
import Btn, { BtnProps } from "@/app/components/Btn";
import Warning from "@/app/icons/warning.svg";
import Close from "@/app/icons/closeIcon.svg";
export interface ModalProps {
onOk?: () => void;
onCancel?: () => void;
okText?: string;
cancelText?: string;
okBtnProps?: BtnProps;
cancelBtnProps?: BtnProps;
content?:
| React.ReactNode
| ((handlers: { close: () => void }) => JSX.Element);
title?: React.ReactNode;
visible?: boolean;
noFooter?: boolean;
noHeader?: boolean;
isMobile?: boolean;
closeble?: boolean;
type?: "modal" | "bottom-drawer";
headerBordered?: boolean;
modelClassName?: string;
onOpen?: (v: boolean) => void;
maskCloseble?: boolean;
}
export interface WarnProps
extends Omit<
ModalProps,
| "closeble"
| "isMobile"
| "noHeader"
| "noFooter"
| "onOk"
| "okBtnProps"
| "cancelBtnProps"
| "content"
> {
onOk?: () => Promise<void> | void;
content?: React.ReactNode;
}
export interface TriggerProps
extends Omit<ModalProps, "visible" | "onOk" | "onCancel"> {
children: JSX.Element;
className?: string;
}
const baseZIndex = 150;
let div: HTMLDivElement | null = null;
const Modal = (props: ModalProps) => {
const {
onOk,
onCancel,
okText,
cancelText,
content,
title,
visible,
noFooter,
noHeader,
closeble = true,
okBtnProps,
cancelBtnProps,
type = "modal",
headerBordered,
modelClassName,
onOpen,
maskCloseble = true,
} = props;
const [open, setOpen] = useState(!!visible);
const mergeOpen = visible ?? open;
useLayoutEffect(() => {
const div: HTMLDivElement = document.createElement("div");
div.id = "confirm-root";
div.style.height = "0px";
document.body.appendChild(div);
}, []);
const root = createRoot(div);
const closeModal = () => {
root.unmount();
};
const handleClose = () => {
setOpen(false);
onCancel?.();
};
const handleOk = () => {
setOpen(false);
onOk?.();
};
useLayoutEffect(() => {
onOpen?.(mergeOpen);
}, [mergeOpen]);
let layoutClassName = "";
let panelClassName = "";
let titleClassName = "";
let footerClassName = "";
switch (type) {
case "bottom-drawer":
layoutClassName = "fixed inset-0 flex flex-col w-[100%] bottom-0";
panelClassName =
"rounded-t-chat-model-select overflow-y-auto overflow-x-hidden";
titleClassName = "px-4 py-3";
footerClassName = "absolute w-[100%]";
break;
case "modal":
default:
layoutClassName =
"fixed inset-0 flex flex-col item-start top-0 left-[50vw] translate-x-[-50%] max-sm:w-modal-modal-type-mobile";
panelClassName = "rounded-lg px-6 sm:w-modal-modal-type";
titleClassName = "py-6 max-sm:pb-3";
footerClassName = "py-6";
}
const btnCommonClass = "px-4 py-2.5 rounded-md max-sm:flex-1";
const { className: okBtnClass } = okBtnProps || {};
const { className: cancelBtnClass } = cancelBtnProps || {};
return (
<AlertDialog.Root open={mergeOpen} onOpenChange={setOpen}>
<AlertDialog.Portal>
<AlertDialog.Overlay
className="fixed inset-0 bg-modal-mask animate-mask "
style={{ zIndex: baseZIndex - 1 }}
onClick={() => {
if (maskCloseble) {
handleClose();
}
}}
/>
<AlertDialog.Content
className={`
${layoutClassName}
`}
style={{ zIndex: baseZIndex - 1 }}
>
<div
className="flex-1"
onClick={() => {
if (maskCloseble) {
handleClose();
}
}}
>
&nbsp;
</div>
<div
className={`flex flex-col flex-0
bg-moda-panel text-modal-panel
${modelClassName}
${panelClassName}
`}
>
{!noHeader && (
<AlertDialog.Title
className={`
flex items-center justify-between gap-3 font-common
md:text-chat-header-title md:font-bold md:leading-5
${
headerBordered
? " border-b border-modal-header-bottom"
: ""
}
${titleClassName}
`}
>
<div className="flex items-center justify-start flex-1 gap-3 text-text-modal-title text-chat-header-title">
{title}
</div>
{closeble && (
<div
className="items-center"
onClick={() => {
handleClose();
}}
>
<Close />
</div>
)}
</AlertDialog.Title>
)}
<div className="flex-1 overflow-hidden text-text-modal-content text-sm-title">
{typeof content === "function"
? content({
close: () => {
handleClose();
},
})
: content}
</div>
{!noFooter && (
<div
className={`
flex gap-3 sm:justify-end max-sm:justify-between
${footerClassName}
`}
>
<AlertDialog.Cancel asChild>
<Btn
{...cancelBtnProps}
onClick={() => handleClose()}
text={cancelText}
className={`${btnCommonClass} ${cancelBtnClass}`}
/>
</AlertDialog.Cancel>
<AlertDialog.Action asChild>
<Btn
{...okBtnProps}
onClick={handleOk}
text={okText}
className={`${btnCommonClass} ${okBtnClass}`}
/>
</AlertDialog.Action>
</div>
)}
</div>
{type === "modal" && (
<div
className="flex-1"
onClick={() => {
if (maskCloseble) {
handleClose();
}
}}
>
&nbsp;
</div>
)}
</AlertDialog.Content>
</AlertDialog.Portal>
</AlertDialog.Root>
);
};
export const Warn = ({
title,
onOk,
visible,
content,
...props
}: WarnProps) => {
const [internalVisible, setVisible] = useState(visible);
return (
<Modal
{...props}
title={
<>
<Warning />
{title}
</>
}
content={
<AlertDialog.Description
className={`
font-common font-normal
md:text-sm-title md:leading-[158%]
`}
>
{content}
</AlertDialog.Description>
}
closeble={false}
onOk={() => {
const toDo = onOk?.();
if (toDo instanceof Promise) {
toDo.then(() => {
setVisible(false);
});
} else {
setVisible(false);
}
}}
visible={internalVisible}
okBtnProps={{
className: `bg-delete-chat-ok-btn text-text-delete-chat-ok-btn `,
}}
cancelBtnProps={{
className: `bg-delete-chat-cancel-btn border border-delete-chat-cancel-btn text-text-delete-chat-cancel-btn`,
}}
/>
);
};
Modal.warn = (props: Omit<WarnProps, "visible" | "onCancel" | "onOk">) => {
const root = createRoot(div);
const closeModal = () => {
root.unmount();
};
return new Promise<boolean>((resolve) => {
root.render(
<Warn
{...props}
visible={true}
onCancel={() => {
closeModal();
resolve(false);
}}
onOk={() => {
closeModal();
resolve(true);
}}
/>,
);
});
};
export const Trigger = (props: TriggerProps) => {
const { children, className, content, ...rest } = props;
const [internalVisible, setVisible] = useState(false);
return (
<>
<div
className={className}
onClick={() => {
setVisible(true);
}}
>
{children}
</div>
<Modal
{...rest}
visible={internalVisible}
onCancel={() => {
setVisible(false);
}}
content={
typeof content === "function"
? content({
close: () => {
setVisible(false);
},
})
: content
}
/>
</>
);
};
Modal.Trigger = Trigger;
export default Modal;

View File

@@ -0,0 +1,366 @@
"use client";
import useRelativePosition from "@/app/hooks/useRelativePosition";
import {
RefObject,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from "react";
import { createPortal } from "react-dom";
const ArrowIcon = ({ sibling }: { sibling: RefObject<HTMLDivElement> }) => {
const [color, setColor] = useState<string>("");
useEffect(() => {
if (sibling.current) {
const { backgroundColor } = window.getComputedStyle(sibling.current);
setColor(backgroundColor);
}
}, []);
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="6"
viewBox="0 0 16 6"
fill="none"
>
<path
d="M16 0H0C1.28058 0 2.50871 0.508709 3.41421 1.41421L6.91 4.91C7.51199 5.51199 8.48801 5.51199 9.09 4.91L12.5858 1.41421C13.4913 0.508708 14.7194 0 16 0Z"
fill={color}
/>
</svg>
);
};
const baseZIndex = 100;
const popoverRootName = "popoverRoot";
export interface PopoverProps {
content?: JSX.Element | string;
children?: JSX.Element;
show?: boolean;
onShow?: (v: boolean) => void;
className?: string;
popoverClassName?: string;
trigger?: "hover" | "click";
placement?: "t" | "lt" | "rt" | "lb" | "rb" | "b" | "l" | "r";
noArrow?: boolean;
delayClose?: number;
useGlobalRoot?: boolean;
getPopoverPanelRef?: (ref: RefObject<HTMLDivElement>) => void;
}
let popoverRoot: HTMLDivElement;
export default function Popover(props: PopoverProps) {
const {
content,
children,
show,
onShow,
className,
popoverClassName,
trigger = "hover",
placement = "t",
noArrow = false,
delayClose = 0,
useGlobalRoot,
getPopoverPanelRef,
} = props;
const [internalShow, setShow] = useState(false);
const { position, getRelativePosition } = useRelativePosition({
delay: 0,
});
const popoverCommonClass = `absolute p-2 box-border`;
const mergedShow = show ?? internalShow;
const { arrowClassName, placementStyle, placementClassName } = useMemo(() => {
const arrowCommonClassName = `${
noArrow ? "hidden" : ""
} absolute z-10 left-[50%] translate-x-[calc(-50%)]`;
let defaultTopPlacement = true; // when users dont config 't' or 'b'
const {
distanceToBottomBoundary = 0,
distanceToLeftBoundary = 0,
distanceToRightBoundary = -10000,
distanceToTopBoundary = 0,
targetH = 0,
targetW = 0,
} = position?.poi || {};
if (distanceToBottomBoundary > distanceToTopBoundary) {
defaultTopPlacement = false;
}
const placements = {
lt: {
placementStyle: {
bottom: `calc(${distanceToBottomBoundary + targetH}px + 0.5rem)`,
left: `calc(${distanceToLeftBoundary}px - ${targetW * 0.02}px)`,
},
arrowClassName: `${arrowCommonClassName} bottom-[calc(100%+0.5rem)] translate-y-[calc(100%)] pb-[0.5rem]`,
placementClassName: "bottom-[calc(100%+0.5rem)] left-[calc(-2%)]",
},
lb: {
placementStyle: {
top: `calc(-${distanceToBottomBoundary}px + 0.5rem)`,
left: `calc(${distanceToLeftBoundary}px - ${targetW * 0.02}px)`,
},
arrowClassName: `${arrowCommonClassName} top-[calc(100%+0.5rem)] translate-y-[calc(-100%)] pt-[0.5rem]`,
placementClassName: "top-[calc(100%+0.5rem)] left-[calc(-2%)]",
},
rt: {
placementStyle: {
bottom: `calc(${distanceToBottomBoundary + targetH}px + 0.5rem)`,
right: `calc(${distanceToRightBoundary}px - ${targetW * 0.02}px)`,
},
arrowClassName: `${arrowCommonClassName} bottom-[calc(100%+0.5rem)] translate-y-[calc(100%)] pb-[0.5rem]`,
placementClassName: "bottom-[calc(100%+0.5rem)] right-[calc(-2%)]",
},
rb: {
placementStyle: {
top: `calc(-${distanceToBottomBoundary}px + 0.5rem)`,
right: `calc(${distanceToRightBoundary}px - ${targetW * 0.02}px)`,
},
arrowClassName: `${arrowCommonClassName} top-[calc(100%+0.5rem)] translate-y-[calc(-100%)] pt-[0.5rem]`,
placementClassName: "top-[calc(100%+0.5rem)] right-[calc(-2%)]",
},
t: {
placementStyle: {
bottom: `calc(${distanceToBottomBoundary + targetH}px + 0.5rem)`,
left: `calc(${distanceToLeftBoundary + targetW / 2}px`,
transform: "translateX(-50%)",
},
arrowClassName: `${arrowCommonClassName} bottom-[calc(100%+0.5rem)] translate-y-[calc(100%)] pb-[0.5rem]`,
placementClassName:
"bottom-[calc(100%+0.5rem)] left-[50%] translate-x-[calc(-50%)]",
},
b: {
placementStyle: {
top: `calc(-${distanceToBottomBoundary}px + 0.5rem)`,
left: `calc(${distanceToLeftBoundary + targetW / 2}px`,
transform: "translateX(-50%)",
},
arrowClassName: `${arrowCommonClassName} top-[calc(100%+0.5rem)] translate-y-[calc(-100%)] pt-[0.5rem]`,
placementClassName:
"top-[calc(100%+0.5rem)] left-[50%] translate-x-[calc(-50%)]",
},
};
const getStyle = () => {
if (["l", "r"].includes(placement)) {
return placements[
`${placement}${defaultTopPlacement ? "t" : "b"}` as
| "lt"
| "lb"
| "rb"
| "rt"
];
}
return placements[placement as Exclude<typeof placement, "l" | "r">];
};
return getStyle();
}, [Object.values(position?.poi || {})]);
const popoverRef = useRef<HTMLDivElement>(null);
const closeTimer = useRef<number>(0);
useLayoutEffect(() => {
if (popoverRoot) {
return;
}
popoverRoot = document.querySelector(
`#${popoverRootName}`,
) as HTMLDivElement;
if (!popoverRoot) {
popoverRoot = document.createElement("div");
document.body.appendChild(popoverRoot);
popoverRoot.style.height = "0px";
popoverRoot.style.width = "100%";
popoverRoot.style.position = "fixed";
popoverRoot.style.bottom = "0";
popoverRoot.style.zIndex = "10000";
popoverRoot.id = "popover-root";
}
}, []);
useLayoutEffect(() => {
getPopoverPanelRef?.(popoverRef);
onShow?.(internalShow);
}, [internalShow]);
if (trigger === "click") {
const handleOpen = (e: { currentTarget: any }) => {
clearTimeout(closeTimer.current);
setShow(true);
getRelativePosition(e.currentTarget, "");
window.document.documentElement.style.overflow = "hidden";
};
const handleClose = () => {
if (delayClose) {
closeTimer.current = window.setTimeout(() => {
setShow(false);
}, delayClose);
} else {
setShow(false);
}
window.document.documentElement.style.overflow = "auto";
};
if (mergedShow) {
return null;
}
return (
<div
className={`relative ${className}`}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
if (!mergedShow) {
handleOpen(e);
} else {
handleClose();
}
}}
>
{children}
{mergedShow && (
<>
{!noArrow && (
<div className={`${arrowClassName}`}>
<ArrowIcon sibling={popoverRef} />
</div>
)}
{createPortal(
<div
className={`${popoverCommonClass} ${popoverClassName} cursor-pointer overflow-auto`}
style={{ zIndex: baseZIndex + 1, ...placementStyle }}
ref={popoverRef}
>
{content}
</div>,
popoverRoot,
)}
{createPortal(
<div
className=" fixed w-[100vw] h-[100vh] right-0 bottom-0"
style={{ zIndex: baseZIndex }}
onClick={(e) => {
e.preventDefault();
handleClose();
}}
>
&nbsp;
</div>,
popoverRoot,
)}
</>
)}
</div>
);
}
if (useGlobalRoot) {
return (
<div
className={`relative ${className}`}
onPointerEnter={(e) => {
e.preventDefault();
clearTimeout(closeTimer.current);
onShow?.(true);
setShow(true);
getRelativePosition(e.currentTarget, "");
window.document.documentElement.style.overflow = "hidden";
}}
onPointerLeave={(e) => {
e.preventDefault();
if (delayClose) {
closeTimer.current = window.setTimeout(() => {
onShow?.(false);
setShow(false);
}, delayClose);
} else {
onShow?.(false);
setShow(false);
}
window.document.documentElement.style.overflow = "auto";
}}
>
{children}
{mergedShow && (
<>
<div
className={`${
noArrow ? "opacity-0" : ""
} bg-inherit ${arrowClassName}`}
style={{ zIndex: baseZIndex + 1 }}
>
<ArrowIcon sibling={popoverRef} />
</div>
{createPortal(
<div
className={` whitespace-nowrap ${popoverCommonClass} ${popoverClassName} cursor-pointer`}
style={{ zIndex: baseZIndex + 1, ...placementStyle }}
ref={popoverRef}
>
{content}
</div>,
popoverRoot,
)}
</>
)}
</div>
);
}
return (
<div
className={`group/popover relative ${className}`}
onPointerEnter={(e) => {
getRelativePosition(e.currentTarget, "");
e.preventDefault();
e.stopPropagation();
}}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
{children}
<div
className={`
hidden group-hover/popover:block
${noArrow ? "opacity-0" : ""}
bg-inherit
${arrowClassName}
`}
style={{ zIndex: baseZIndex + 1 }}
>
<ArrowIcon sibling={popoverRef} />
</div>
<div
className={`
hidden group-hover/popover:block whitespace-nowrap
${popoverCommonClass}
${placementClassName}
${popoverClassName}
`}
ref={popoverRef}
style={{ zIndex: baseZIndex + 1 }}
>
{content}
</div>
</div>
);
}

View File

@@ -0,0 +1,71 @@
import { useMemo, ReactNode } from "react";
import { Path, SIDEBAR_ID, SlotID } from "@/app/constant";
import { getLang } from "@/app/locales";
import useMobileScreen from "@/app/hooks/useMobileScreen";
import useListenWinResize from "@/app/hooks/useListenWinResize";
import { usePathname } from "next/navigation";
import { useDeviceInfo } from "@/app/hooks/useDeviceInfo";
interface ScreenProps {
children: ReactNode;
noAuth: ReactNode;
sidebar: ReactNode;
}
export default function Screen(props: ScreenProps) {
const pathname = usePathname();
const isAuth = pathname === Path.Auth;
const isMobileScreen = useMobileScreen();
const { deviceType, systemInfo } = useDeviceInfo();
useListenWinResize();
return (
<div
className={`
flex h-[100%] w-[100%] bg-center
max-md:relative max-md:flex-col-reverse max-md:bg-global-mobile
md:overflow-hidden md:bg-global
`}
style={{
direction: getLang() === "ar" ? "rtl" : "ltr",
}}
>
{isAuth ? (
props.noAuth
) : (
<>
<div
className={`
max-md:absolute max-md:w-[100%] max-md:bottom-0 max-md:z-10
md:flex-0 md:overflow-hidden
`}
id={SIDEBAR_ID}
>
{props.sidebar}
</div>
<div
className={`
h-[100%]
max-md:w-[100%]
md:flex-1 md:min-w-0 md:overflow-hidden md:flex
`}
id={SlotID.AppBody}
style={{
// #3016 disable transition on ios mobile screen
transition:
systemInfo === "iOS" && deviceType === "mobile"
? "none"
: undefined,
}}
>
{props.children}
</div>
</>
)}
</div>
);
}

View File

@@ -0,0 +1,24 @@
.search {
display: flex;
max-width: 460px;
height: 50px;
padding: 16px;
align-items: center;
gap: 8px;
flex-shrink: 0;
border-radius: 16px;
border: 1px solid var(--Light-Text-Black, #18182A);
background: var(--light-opacity-white-70, rgba(255, 255, 255, 0.70));
box-shadow: 0px 8px 40px 0px rgba(60, 68, 255, 0.12);
.icon {
height: 20px;
width: 20px;
flex: 0 0;
}
.input {
height: 18px;
flex: 1 1;
}
}

View File

@@ -0,0 +1,30 @@
import styles from "./index.module.scss";
import SearchIcon from "@/app/icons/search.svg";
export interface SearchProps {
value?: string;
onSearch?: (v: string) => void;
placeholder?: string;
}
const Search = (props: SearchProps) => {
const { placeholder = "", value, onSearch } = props;
return (
<div className={styles["search"]}>
<div className={styles["icon"]}>
<SearchIcon />
</div>
<input
className={styles["input"]}
placeholder={placeholder}
value={value}
onChange={(e) => {
e.preventDefault();
onSearch?.(e.target.value);
}}
/>
</div>
);
};
export default Search;

View File

@@ -0,0 +1,118 @@
import SelectIcon from "@/app/icons/downArrowIcon.svg";
import Popover from "@/app/components/Popover";
import React, { useContext, useMemo, useRef } from "react";
import useRelativePosition, {
Orientation,
} from "@/app/hooks/useRelativePosition";
import List from "@/app/components/List";
import Selected from "@/app/icons/selectedIcon.svg";
export type Option<Value> = {
value: Value;
label: string;
icon?: React.ReactNode;
};
export interface SearchProps<Value> {
value?: string;
onSelect?: (v: Value) => void;
options?: Option<Value>[];
inMobile?: boolean;
}
const Select = <Value extends number | string>(props: SearchProps<Value>) => {
const { value, onSelect, options = [], inMobile } = props;
const { isMobileScreen, selectClassName } = useContext(List.ListContext);
const optionsRef = useRef<Option<Value>[]>([]);
optionsRef.current = options;
const selectedOption = useMemo(
() => optionsRef.current.find((o) => o.value === value),
[value],
);
const contentRef = useRef<HTMLDivElement>(null);
const { position, getRelativePosition } = useRelativePosition({
delay: 0,
});
let headerH = 100;
let baseH = position?.poi.distanceToBottomBoundary || 0;
if (isMobileScreen) {
headerH = 60;
}
if (position?.poi.relativePosition[1] === Orientation.bottom) {
baseH = position?.poi.distanceToTopBoundary;
}
const maxHeight = `${baseH - headerH}px`;
const content = (
<div
className={` flex flex-col gap-1 overflow-y-auto overflow-x-hidden`}
style={{ maxHeight }}
>
{options?.map((o) => (
<div
key={o.value}
className={`
flex items-center px-3 py-2 gap-3 rounded-action-btn hover:bg-select-option-hovered cursor-pointer
`}
onClick={() => {
onSelect?.(o.value);
}}
>
<div className="flex gap-2 flex-1 follow-parent-svg text-text-select-option">
{!!o.icon && <div className="flex items-center">{o.icon}</div>}
<div className={`flex-1 text-text-select-option`}>{o.label}</div>
</div>
<div
className={
selectedOption?.value === o.value ? "opacity-100" : "opacity-0"
}
>
<Selected />
</div>
</div>
))}
</div>
);
return (
<Popover
content={content}
trigger="click"
noArrow
placement={
position?.poi.relativePosition[1] !== Orientation.bottom ? "rb" : "rt"
}
popoverClassName="border border-select-popover rounded-lg shadow-select-popover-shadow w-actions-popover bg-select-popover-panel"
onShow={(e) => {
getRelativePosition(contentRef.current!, "");
}}
className={selectClassName}
>
<div
className={`flex items-center gap-3 py-2 px-3 bg-select rounded-action-btn font-common text-sm-title cursor-pointer hover:bg-select-hover transition duration-300 ease-in-out`}
ref={contentRef}
>
<div
className={`flex items-center gap-2 flex-1 follow-parent-svg text-text-select`}
>
{!!selectedOption?.icon && (
<div className={``}>{selectedOption?.icon}</div>
)}
<div className={`flex-1`}>{selectedOption?.label}</div>
</div>
<div className={``}>
<SelectIcon />
</div>
</div>
</Popover>
);
};
export default Select;

View File

@@ -0,0 +1,99 @@
import { useContext, useEffect, useRef } from "react";
import { ListContext } from "@/app/components/List";
import { useResizeObserver } from "usehooks-ts";
interface SlideRangeProps {
className?: string;
description?: string;
range?: {
start?: number;
stroke?: number;
};
onSlide?: (v: number) => void;
value?: number;
step?: number;
}
const margin = 15;
export default function SlideRange(props: SlideRangeProps) {
const {
className = "",
description = "",
range = {},
value,
onSlide,
step,
} = props;
const { start = 0, stroke = 1 } = range;
const { rangeClassName, update } = useContext(ListContext);
const slideRef = useRef<HTMLDivElement>(null);
useResizeObserver({
ref: slideRef,
onResize: () => {
setProperty(value);
},
});
const transformToWidth = (x: number = start) => {
const abs = x - start;
const maxWidth = (slideRef.current?.clientWidth || 1) - margin * 2;
const result = (abs / stroke) * maxWidth;
return result;
};
const setProperty = (value?: number) => {
const initWidth = transformToWidth(value);
slideRef.current?.style.setProperty(
"--slide-value-size",
`${initWidth + margin}px`,
);
};
useEffect(() => {
update?.({ type: "range" });
}, []);
return (
<div
className={`flex flex-col justify-center items-end gap-1 w-[100%] ${className} ${rangeClassName}`}
>
{!!description && (
<div className=" text-common text-sm ">{description}</div>
)}
<div
className="flex my-1.5 relative w-[100%] h-1.5 bg-slider rounded-slide cursor-pointer"
ref={slideRef}
>
<div className="cursor-pointer absolute marker:top-0 h-[100%] w-[var(--slide-value-size)] bg-slider-slided-travel rounded-slide">
&nbsp;
</div>
<div
className="cursor-pointer absolute z-1 w-[30px] top-[50%] translate-y-[-50%] left-[var(--slide-value-size)] translate-x-[-50%] h-slide-btn leading-slide-btn text-sm-mobile text-center rounded-slide border border-slider-block bg-slider-block hover:bg-slider-block-hover text-text-slider-block"
// onPointerDown={onPointerDown}
>
{value}
</div>
<input
type="range"
className="w-[100%] h-[100%] opacity-0 cursor-pointer"
value={value}
min={start}
max={start + stroke}
step={step}
onChange={(e) => {
setProperty(e.target.valueAsNumber);
onSlide?.(e.target.valueAsNumber);
}}
style={{
marginLeft: margin,
marginRight: margin,
}}
/>
</div>
</div>
);
}

View File

@@ -0,0 +1,33 @@
import * as RadixSwitch from "@radix-ui/react-switch";
import { useContext } from "react";
import List from "../List";
interface SwitchProps {
value: boolean;
onChange: (v: boolean) => void;
}
export default function Switch(props: SwitchProps) {
const { value, onChange } = props;
const { switchClassName = "" } = useContext(List.ListContext);
return (
<RadixSwitch.Root
checked={value}
onCheckedChange={onChange}
className={`
cursor-pointer flex w-switch h-switch p-0.5 box-content rounded-md transition-colors duration-300 ease-in-out
${switchClassName}
${
value
? "bg-switch-checked justify-end"
: "bg-switch-unchecked justify-start"
}
`}
>
<RadixSwitch.Thumb
className={` bg-switch-btn block w-4 h-4 drop-shadow-sm rounded-md`}
/>
</RadixSwitch.Root>
);
}

View File

@@ -0,0 +1,27 @@
import ImgDeleteIcon from "@/app/icons/imgDeleteIcon.svg";
export interface ThumbnailProps {
image: string;
deleteImage: () => void;
}
export default function Thumbnail(props: ThumbnailProps) {
const { image, deleteImage } = props;
return (
<div
className={` h-thumbnail w-thumbnail cursor-default border border-thumbnail rounded-action-btn flex-0 bg-cover bg-center`}
style={{ backgroundImage: `url("${image}")` }}
>
<div
className={` w-[100%] h-[100%] opacity-0 transition-all duration-200 rounded-action-btn hover:opacity-100 hover:bg-thumbnail-mask`}
>
<div
className={`cursor-pointer flex items-center justify-center float-right`}
onClick={deleteImage}
>
<ImgDeleteIcon />
</div>
</div>
</div>
);
}

View File

@@ -6,6 +6,8 @@
width: 100%;
flex-direction: column;
background-color: var(--white);
.auth-logo {
transform: scale(1.4);
}
@@ -33,4 +35,18 @@
margin-bottom: 10px;
}
}
input[type="number"],
input[type="text"],
input[type="password"] {
appearance: none;
border-radius: 10px;
border: var(--border-in-light);
min-height: 36px;
box-sizing: border-box;
background: var(--white);
color: var(--black);
padding: 0 10px;
max-width: 50%;
font-family: inherit;
}
}

View File

@@ -1,18 +1,35 @@
"use client";
import styles from "./auth.module.scss";
import { IconButton } from "./button";
import { useNavigate } from "react-router-dom";
import { useRouter } from "next/navigation";
import { Path } from "../constant";
import { useAccessStore } from "../store";
import Locale from "../locales";
import BotIcon from "../icons/bot.svg";
import { useEffect } from "react";
import { getClientConfig } from "../config/client";
export function AuthPage() {
const navigate = useNavigate();
const access = useAccessStore();
const router = useRouter();
const accessStore = useAccessStore();
const goHome = () => navigate(Path.Home);
const goHome = () => router.push(Path.Home);
const goChat = () => router.push(Path.Chat);
const resetAccessCode = () => {
accessStore.update((access) => {
access.openaiApiKey = "";
access.accessCode = "";
});
}; // Reset access code to empty string
useEffect(() => {
if (getClientConfig()?.isApp) {
navigate(Path.Settings);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div className={styles["auth-page"]}>
@@ -27,19 +44,54 @@ export function AuthPage() {
className={styles["auth-input"]}
type="password"
placeholder={Locale.Auth.Input}
value={access.accessCode}
value={accessStore.accessCode}
onChange={(e) => {
access.updateCode(e.currentTarget.value);
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}
value={accessStore.openaiApiKey}
onChange={(e) => {
accessStore.update(
(access) => (access.openaiApiKey = e.currentTarget.value),
);
}}
/>
<input
className={styles["auth-input"]}
type="password"
placeholder={Locale.Settings.Access.Google.ApiKey.Placeholder}
value={accessStore.googleApiKey}
onChange={(e) => {
accessStore.update(
(access) => (access.googleApiKey = e.currentTarget.value),
);
}}
/>
</>
) : null}
<div className={styles["auth-actions"]}>
<IconButton
text={Locale.Auth.Confirm}
type="primary"
onClick={goHome}
onClick={goChat}
/>
<IconButton
text={Locale.Auth.Later}
onClick={() => {
resetAccessCode();
goHome();
}}
/>
<IconButton text={Locale.Auth.Later} onClick={goHome} />
</div>
</div>
);

View File

@@ -27,6 +27,26 @@
fill: white !important;
}
}
&.danger {
color: rgba($color: red, $alpha: 0.8);
border-color: rgba($color: red, $alpha: 0.5);
background-color: rgba($color: red, $alpha: 0.05);
&:hover {
border-color: red;
background-color: rgba($color: red, $alpha: 0.1);
}
path {
fill: red !important;
}
}
&:hover,
&:focus {
border-color: var(--primary);
}
}
.shadow {
@@ -37,10 +57,6 @@
border: var(--border-in-light);
}
.icon-button:hover {
border-color: var(--primary);
}
.icon-button-icon {
width: 16px;
height: 16px;
@@ -56,9 +72,12 @@
}
.icon-button-text {
margin-left: 5px;
font-size: 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&:not(:first-child) {
margin-left: 5px;
}
}

View File

@@ -2,16 +2,20 @@ import * as React from "react";
import styles from "./button.module.scss";
export type ButtonType = "primary" | "danger" | null;
export function IconButton(props: {
onClick?: () => void;
icon?: JSX.Element;
type?: "primary" | "danger";
type?: ButtonType;
text?: string;
bordered?: boolean;
shadow?: boolean;
className?: string;
title?: string;
disabled?: boolean;
tabIndex?: number;
autoFocus?: boolean;
}) {
return (
<button
@@ -25,6 +29,8 @@ export function IconButton(props: {
title={props.title}
disabled={props.disabled}
role="button"
tabIndex={props.tabIndex}
autoFocus={props.autoFocus}
>
{props.icon && (
<div

View File

@@ -12,11 +12,14 @@ import {
import { useChatStore } from "../store";
import Locale from "../locales";
import { Link, useNavigate } from "react-router-dom";
// import { Link, 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 { usePathname, useRouter } from "next/navigation";
export function ChatItem(props: {
onClick?: () => void;
@@ -25,7 +28,7 @@ export function ChatItem(props: {
count: number;
time: string;
selected: boolean;
id: number;
id: string;
index: number;
narrow?: boolean;
mask: Mask;
@@ -38,12 +41,16 @@ export function ChatItem(props: {
});
}
}, [props.selected]);
const pathname = usePathname();
return (
<Draggable draggableId={`${props.id}`} index={props.index}>
{(provided) => (
<div
className={`${styles["chat-item"]} ${
props.selected && styles["chat-item-selected"]
props.selected &&
(pathname === Path.Chat || pathname === Path.Home) &&
styles["chat-item-selected"]
}`}
onClick={props.onClick}
ref={(ele) => {
@@ -59,7 +66,10 @@ export function ChatItem(props: {
{props.narrow ? (
<div className={styles["chat-item-narrow"]}>
<div className={styles["chat-item-avatar"] + " no-dark"}>
<MaskAvatar mask={props.mask} />
<MaskAvatar
avatar={props.mask.avatar}
model={props.mask.modelConfig.model}
/>
</div>
<div className={styles["chat-item-narrow-count"]}>
{props.count}
@@ -79,7 +89,11 @@ export function ChatItem(props: {
<div
className={styles["chat-item-delete"]}
onClickCapture={props.onDelete}
onClickCapture={(e) => {
props.onDelete?.();
e.preventDefault();
e.stopPropagation();
}}
>
<DeleteIcon />
</div>
@@ -99,7 +113,8 @@ export function ChatList(props: { narrow?: boolean }) {
],
);
const chatStore = useChatStore();
const navigate = useNavigate();
const isMobileScreen = useMobileScreen();
const router = useRouter();
const onDragEnd: OnDragEndResponder = (result) => {
const { destination, source } = result;
@@ -136,11 +151,15 @@ export function ChatList(props: { narrow?: boolean }) {
index={i}
selected={i === selectedIndex}
onClick={() => {
navigate(Path.Chat);
// navigate(Path.Chat);
router.push(Path.Chat);
selectSession(i);
}}
onDelete={() => {
if (!props.narrow || confirm(Locale.Home.DeleteChat)) {
onDelete={async () => {
if (
(!props.narrow && !isMobileScreen) ||
(await showConfirm(Locale.Home.DeleteChat))
) {
chatStore.deleteSession(i);
}
}}

View File

@@ -1,5 +1,47 @@
@import "../styles/animation.scss";
.attach-images {
position: absolute;
left: 30px;
bottom: 32px;
display: flex;
}
.attach-image {
cursor: default;
width: 64px;
height: 64px;
border: rgba($color: #888, $alpha: 0.2) 1px solid;
border-radius: 5px;
margin-right: 10px;
background-size: cover;
background-position: center;
background-color: var(--white);
.attach-image-mask {
width: 100%;
height: 100%;
opacity: 0;
transition: all ease 0.2s;
}
.attach-image-mask:hover {
opacity: 1;
}
.delete-image {
width: 24px;
height: 24px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
border-radius: 5px;
float: right;
background-color: var(--white);
}
}
.chat-input-actions {
display: flex;
flex-wrap: wrap;
@@ -14,11 +56,11 @@
padding: 4px 10px;
animation: slide-in ease 0.3s;
box-shadow: var(--card-shadow);
transition: all ease 0.3s;
margin-bottom: 10px;
transition: width ease 0.3s;
align-items: center;
height: 16px;
width: var(--icon-width);
overflow: hidden;
&:not(:last-child) {
margin-right: 5px;
@@ -30,14 +72,16 @@
opacity: 0;
transform: translateX(-5px);
transition: all ease 0.3s;
transition-delay: 0.1s;
pointer-events: none;
}
&:hover {
--delay: 0.5s;
width: var(--full-width);
transition-delay: var(--delay);
.text {
transition-delay: var(--delay);
opacity: 1;
transform: translate(0);
}
@@ -96,11 +140,41 @@
}
.context-prompt {
.context-prompt-insert {
display: flex;
justify-content: center;
padding: 4px;
opacity: 0.2;
transition: all ease 0.3s;
background-color: rgba(0, 0, 0, 0);
cursor: pointer;
border-radius: 4px;
margin-top: 4px;
margin-bottom: 4px;
&:hover {
opacity: 1;
background-color: rgba(0, 0, 0, 0.05);
}
}
.context-prompt-row {
display: flex;
justify-content: center;
width: 100%;
margin-bottom: 10px;
&:hover {
.context-drag {
opacity: 1;
}
}
.context-drag {
display: flex;
align-items: center;
opacity: 0.5;
transition: all ease 0.3s;
}
.context-role {
margin-right: 10px;
@@ -157,12 +231,10 @@
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 {
@@ -202,3 +274,354 @@
}
}
}
.chat {
display: flex;
flex-direction: column;
position: relative;
height: 100%;
}
.chat-body {
flex: 1;
overflow: auto;
overflow-x: hidden;
padding: 20px;
padding-bottom: 40px;
position: relative;
overscroll-behavior: none;
}
.chat-body-main-title {
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
@media only screen and (max-width: 600px) {
.chat-body-title {
text-align: center;
}
}
.chat-message {
display: flex;
flex-direction: row;
&:last-child {
animation: slide-in ease 0.3s;
}
}
.chat-message-user {
display: flex;
flex-direction: row-reverse;
.chat-message-header {
flex-direction: row-reverse;
}
}
.chat-message-header {
margin-top: 20px;
display: flex;
align-items: center;
.chat-message-actions {
display: flex;
box-sizing: border-box;
font-size: 12px;
align-items: flex-end;
justify-content: space-between;
transition: all ease 0.3s;
transform: scale(0.9) translateY(5px);
margin: 0 10px;
opacity: 0;
pointer-events: none;
.chat-input-actions {
display: flex;
flex-wrap: nowrap;
}
}
}
.chat-message-container {
max-width: var(--message-max-width);
display: flex;
flex-direction: column;
align-items: flex-start;
&:hover {
.chat-message-edit {
opacity: 0.9;
}
.chat-message-actions {
opacity: 1;
pointer-events: all;
transform: scale(1) translateY(0);
}
}
}
.chat-message-user>.chat-message-container {
align-items: flex-end;
}
.chat-message-avatar {
position: relative;
.chat-message-edit {
position: absolute;
height: 100%;
width: 100%;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: all ease 0.3s;
button {
padding: 7px;
}
}
/* Specific styles for iOS devices */
@media screen and (max-device-width: 812px) and (-webkit-min-device-pixel-ratio: 2) {
@supports (-webkit-touch-callout: none) {
.chat-message-edit {
top: -8%;
}
}
}
}
.chat-message-status {
font-size: 12px;
color: #aaa;
line-height: 1.5;
margin-top: 5px;
}
.chat-message-item {
box-sizing: border-box;
max-width: 100%;
margin-top: 10px;
border-radius: 10px;
background-color: rgba(0, 0, 0, 0.05);
padding: 10px;
font-size: 14px;
user-select: text;
word-break: break-word;
border: var(--border-in-light);
position: relative;
transition: all ease 0.3s;
}
.chat-message-item-image {
width: 100%;
margin-top: 10px;
}
.chat-message-item-images {
width: 100%;
display: grid;
justify-content: left;
grid-gap: 10px;
grid-template-columns: repeat(var(--image-count), auto);
margin-top: 10px;
}
.chat-message-item-image-multi {
object-fit: cover;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
.chat-message-item-image,
.chat-message-item-image-multi {
box-sizing: border-box;
border-radius: 10px;
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));
.chat-message-item-image-multi {
width: $calc-image-width;
height: $calc-image-width;
}
.chat-message-item-image {
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));
.chat-message-item-image-multi {
width: $image-width;
height: $image-width;
max-width: $max-image-width;
max-height: $max-image-width;
}
.chat-message-item-image {
max-width: calc(calc(1200px - var(--sidebar-width))/3*2);
}
}
.chat-message-action-date {
font-size: 12px;
opacity: 0.2;
white-space: nowrap;
transition: all ease 0.6s;
color: var(--black);
text-align: right;
width: 100%;
box-sizing: border-box;
padding-right: 10px;
pointer-events: none;
z-index: 1;
}
.chat-message-user>.chat-message-container>.chat-message-item {
background-color: var(--second);
&:hover {
min-width: 0;
}
}
.chat-input-panel {
position: relative;
width: 100%;
padding: 20px;
padding-top: 10px;
box-sizing: border-box;
flex-direction: column;
border-top: var(--border-in-light);
box-shadow: var(--card-shadow);
.chat-input-actions {
.chat-input-action {
margin-bottom: 10px;
}
}
}
@mixin single-line {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.prompt-hints {
min-height: 20px;
width: 100%;
max-height: 50vh;
overflow: auto;
display: flex;
flex-direction: column-reverse;
background-color: var(--white);
border: var(--border-in-light);
border-radius: 10px;
margin-bottom: 10px;
box-shadow: var(--shadow);
.prompt-hint {
color: var(--black);
padding: 6px 10px;
animation: slide-in ease 0.3s;
cursor: pointer;
transition: all ease 0.3s;
border: transparent 1px solid;
margin: 4px;
border-radius: 8px;
&:not(:last-child) {
margin-top: 0;
}
.hint-title {
font-size: 12px;
font-weight: bolder;
@include single-line();
}
.hint-content {
font-size: 12px;
@include single-line();
}
&-selected,
&:hover {
border-color: var(--primary);
}
}
}
.chat-input-panel-inner {
cursor: text;
display: flex;
flex: 1;
border-radius: 10px;
border: var(--border-in-light);
}
.chat-input-panel-inner-attach {
padding-bottom: 80px;
}
.chat-input-panel-inner:has(.chat-input:focus) {
border: 1px solid var(--primary);
}
.chat-input {
height: 100%;
width: 100%;
border-radius: 10px;
border: none;
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.03);
background-color: var(--white);
color: var(--black);
font-family: inherit;
padding: 10px 90px 10px 14px;
resize: none;
outline: none;
box-sizing: border-box;
min-height: 68px;
}
.chat-input:focus {}
.chat-input-send {
background-color: var(--primary);
color: white;
position: absolute;
right: 30px;
bottom: 32px;
}
@media only screen and (max-width: 600px) {
.chat-input {
font-size: 16px;
}
.chat-input-send {
bottom: 30px;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -10,7 +10,10 @@ import BotIcon from "../icons/bot.svg";
import BlackBotIcon from "../icons/black-bot.svg";
export function getEmojiUrl(unified: string, style: EmojiStyle) {
return `https://cdn.staticfile.org/emoji-datasource-apple/14.0.0/img/${style}/64/${unified}.png`;
// Whoever owns this Content Delivery Network (CDN), I am using your CDN to serve emojis
// Old CDN broken, so I had to switch to this one
// Author: https://github.com/H0llyW00dzZ
return `https://fastly.jsdelivr.net/npm/emoji-datasource-apple/img/${style}/64/${unified}.png`;
}
export function AvatarPicker(props: {
@@ -18,6 +21,7 @@ export function AvatarPicker(props: {
}) {
return (
<EmojiPicker
width={"100%"}
lazyLoadEmojis
theme={EmojiTheme.AUTO}
getEmojiUrl={getEmojiUrl}

View File

@@ -4,7 +4,8 @@ import GithubIcon from "../icons/github.svg";
import ResetIcon from "../icons/reload.svg";
import { ISSUE_URL } from "../constant";
import Locale from "../locales";
import { downloadAs } from "../utils";
import { showConfirm } from "./ui-lib";
import { useSyncStore } from "../store/sync";
interface IErrorBoundaryState {
hasError: boolean;
@@ -25,10 +26,7 @@ export class ErrorBoundary extends React.Component<any, IErrorBoundaryState> {
clearAndSaveData() {
try {
downloadAs(
JSON.stringify(localStorage),
"chatgpt-next-web-snapshot.json",
);
useSyncStore.getState().export();
} finally {
localStorage.clear();
location.reload();
@@ -57,10 +55,11 @@ export class ErrorBoundary extends React.Component<any, IErrorBoundaryState> {
<IconButton
icon={<ResetIcon />}
text="Clear All Data"
onClick={() =>
confirm(Locale.Settings.Actions.ConfirmClearAll) &&
this.clearAndSaveData()
}
onClick={async () => {
if (await showConfirm(Locale.Settings.Danger.Reset.Confirm)) {
this.clearAndSaveData();
}
}}
bordered
/>
</div>

View File

@@ -2,6 +2,9 @@
&-body {
margin-top: 20px;
}
div:not(.no-dark) > svg {
filter: invert(0.5);
}
}
.export-content {
@@ -94,6 +97,7 @@
button {
flex-grow: 1;
&:not(:last-child) {
margin-right: 10px;
}
@@ -186,9 +190,63 @@
box-shadow: var(--card-shadow);
border: var(--border-in-light);
* {
code,
pre {
overflow: hidden;
}
.message-image {
width: 100%;
margin-top: 10px;
}
.message-images {
display: grid;
justify-content: left;
grid-gap: 10px;
grid-template-columns: repeat(var(--image-count), auto);
margin-top: 10px;
}
@media screen and (max-width: 600px) {
$image-width: calc(calc(100vw/2)/var(--image-count));
.message-image-multi {
width: $image-width;
height: $image-width;
}
.message-image {
max-width: calc(100vw/3*2);
}
}
@media screen and (min-width: 600px) {
$max-image-width: calc(900px/3*2/var(--image-count));
$image-width: calc(80vw/3*2/var(--image-count));
.message-image-multi {
width: $image-width;
height: $image-width;
max-width: $max-image-width;
max-height: $max-image-width;
}
.message-image {
max-width: calc(100vw/3*2);
}
}
.message-image-multi {
object-fit: cover;
}
.message-image,
.message-image-multi {
box-sizing: border-box;
border-radius: 10px;
border: rgba($color: #888, $alpha: 0.2) 1px solid;
}
}
&-assistant {
@@ -212,6 +270,5 @@
}
}
.default-theme {
}
}
.default-theme {}
}

View File

@@ -1,9 +1,23 @@
import { ChatMessage, useAppConfig, useChatStore } from "../store";
/* eslint-disable @next/next/no-img-element */
import { ChatMessage, ModelType, useAppConfig, useChatStore } from "../store";
import Locale from "../locales";
import styles from "./exporter.module.scss";
import { List, ListItem, Modal, Select, showToast } from "./ui-lib";
import {
List,
ListItem,
Modal,
Select,
showImageModal,
showModal,
showToast,
} from "./ui-lib";
import { IconButton } from "./button";
import { copyToClipboard, downloadAs, useMobileScreen } from "../utils";
import {
copyToClipboard,
downloadAs,
getMessageImages,
useMobileScreen,
} from "../utils";
import CopyIcon from "../icons/copy.svg";
import LoadingIcon from "../icons/three-dots.svg";
@@ -18,11 +32,15 @@ import { Avatar } from "./emoji";
import dynamic from "next/dynamic";
import NextImage from "next/image";
import { toBlob, toJpeg, toPng } from "html-to-image";
import { toBlob, toPng } from "html-to-image";
import { DEFAULT_MASK_AVATAR } from "../store/mask";
import { api } from "../client/api";
import { prettyObject } from "../utils/format";
import { EXPORT_MESSAGE_CLASS_NAME } from "../constant";
import { EXPORT_MESSAGE_CLASS_NAME, ModelProvider } from "../constant";
import { getClientConfig } from "../config/client";
import { ClientApi } from "../client/api";
import { getMessageTextContent } from "../utils";
import { identifyDefaultClaudeModel } from "../utils/checkers";
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
loading: () => <LoadingIcon />,
@@ -31,7 +49,22 @@ const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
export function ExportMessageModal(props: { onClose: () => void }) {
return (
<div className="modal-mask">
<Modal title={Locale.Export.Title} onClose={props.onClose}>
<Modal
title={Locale.Export.Title}
onClose={props.onClose}
footer={
<div
style={{
width: "100%",
textAlign: "center",
fontSize: 14,
opacity: 0.5,
}}
>
{Locale.Exporter.Description.Title}
</div>
}
>
<div style={{ minHeight: "40vh" }}>
<MessageExporter />
</div>
@@ -117,7 +150,7 @@ export function MessageExporter() {
];
const { currentStep, setCurrentStepIndex, currentStepIndex } =
useSteps(steps);
const formats = ["text", "image"] as const;
const formats = ["text", "image", "json"] as const;
type ExportFormat = (typeof formats)[number];
const [exportConfig, setExportConfig] = useState({
@@ -139,7 +172,7 @@ export function MessageExporter() {
if (exportConfig.includeContext) {
ret.push(...session.mask.context);
}
ret.push(...session.messages.filter((m, i) => selection.has(m.id ?? i)));
ret.push(...session.messages.filter((m) => selection.has(m.id)));
return ret;
}, [
exportConfig.includeContext,
@@ -147,7 +180,21 @@ export function MessageExporter() {
session.mask.context,
selection,
]);
function preview() {
if (exportConfig.format === "text") {
return (
<MarkdownPreviewer messages={selectedMessages} topic={session.topic} />
);
} else if (exportConfig.format === "json") {
return (
<JsonPreviewer messages={selectedMessages} topic={session.topic} />
);
} else {
return (
<ImagePreviewer messages={selectedMessages} topic={session.topic} />
);
}
}
return (
<>
<Steps
@@ -202,16 +249,7 @@ export function MessageExporter() {
/>
</div>
{currentStep.value === "preview" && (
<div className={styles["message-exporter-body"]}>
{exportConfig.format === "text" ? (
<MarkdownPreviewer
messages={selectedMessages}
topic={session.topic}
/>
) : (
<ImagePreviewer messages={selectedMessages} topic={session.topic} />
)}
</div>
<div className={styles["message-exporter-body"]}>{preview()}</div>
)}
</>
);
@@ -234,17 +272,19 @@ export function RenderExport(props: {
return;
}
const renderMsgs = messages.map((v) => {
const [_, role] = v.id.split(":");
const renderMsgs = messages.map((v, i) => {
const [role, _] = v.id.split(":");
return {
id: i.toString(),
role: role as any,
content: v.innerHTML,
content: role === "user" ? v.textContent ?? "" : v.innerHTML,
date: "",
};
});
props.onRender(renderMsgs);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div ref={domRef}>
@@ -254,7 +294,7 @@ export function RenderExport(props: {
id={`${m.role}:${i}`}
className={EXPORT_MESSAGE_CLASS_NAME}
>
<Markdown content={m.content} defaultShow />
<Markdown content={getMessageTextContent(m)} defaultShow />
</div>
))}
</div>
@@ -269,15 +309,47 @@ export function PreviewActions(props: {
}) {
const [loading, setLoading] = useState(false);
const [shouldExport, setShouldExport] = useState(false);
const config = useAppConfig();
const onRenderMsgs = (msgs: ChatMessage[]) => {
setShouldExport(false);
var api: ClientApi;
if (config.modelConfig.model.startsWith("gemini")) {
api = new ClientApi(ModelProvider.GeminiPro);
} else if (identifyDefaultClaudeModel(config.modelConfig.model)) {
api = new ClientApi(ModelProvider.Claude);
} else {
api = new ClientApi(ModelProvider.GPT);
}
api
.share(msgs)
.then((res) => {
if (!res) return;
copyToClipboard(res);
showModal({
title: Locale.Export.Share,
children: [
<input
type="text"
value={res}
key="input"
style={{
width: "100%",
maxWidth: "unset",
}}
readOnly
onClick={(e) => e.currentTarget.select()}
></input>,
],
actions: [
<IconButton
icon={<CopyIcon />}
text={Locale.Chat.Actions.Copy}
key="copy"
onClick={() => copyToClipboard(res)}
/>,
],
});
setTimeout(() => {
window.open(res, "_blank");
}, 800);
@@ -344,7 +416,7 @@ export function PreviewActions(props: {
function ExportAvatar(props: { avatar: string }) {
if (props.avatar === DEFAULT_MASK_AVATAR) {
return (
<NextImage
<img
src={BotIcon.src}
width={30}
height={30}
@@ -354,7 +426,7 @@ function ExportAvatar(props: { avatar: string }) {
);
}
return <Avatar avatar={props.avatar}></Avatar>;
return <Avatar avatar={props.avatar} />;
}
export function ImagePreviewer(props: {
@@ -369,6 +441,7 @@ export function ImagePreviewer(props: {
const previewRef = useRef<HTMLDivElement>(null);
const copy = () => {
showToast(Locale.Export.Image.Toast);
const dom = previewRef.current;
if (!dom) return;
toBlob(dom).then((blob) => {
@@ -382,6 +455,7 @@ export function ImagePreviewer(props: {
])
.then(() => {
showToast(Locale.Copy.Success);
refreshPreview();
});
} catch (e) {
console.error("[Copy Image] ", e);
@@ -392,26 +466,62 @@ export function ImagePreviewer(props: {
const isMobile = useMobileScreen();
const download = () => {
const download = async () => {
showToast(Locale.Export.Image.Toast);
const dom = previewRef.current;
if (!dom) return;
toPng(dom)
.then((blob) => {
if (!blob) return;
if (isMobile) {
const image = new Image();
image.src = blob;
const win = window.open("");
win?.document.write(image.outerHTML);
const isApp = getClientConfig()?.isApp;
try {
const blob = await toPng(dom);
if (!blob) return;
if (isMobile || (isApp && window.__TAURI__)) {
if (isApp && window.__TAURI__) {
const result = await window.__TAURI__.dialog.save({
defaultPath: `${props.topic}.png`,
filters: [
{
name: "PNG Files",
extensions: ["png"],
},
{
name: "All Files",
extensions: ["*"],
},
],
});
if (result !== null) {
const response = await fetch(blob);
const buffer = await response.arrayBuffer();
const uint8Array = new Uint8Array(buffer);
await window.__TAURI__.fs.writeBinaryFile(result, uint8Array);
showToast(Locale.Download.Success);
} else {
showToast(Locale.Download.Failed);
}
} else {
const link = document.createElement("a");
link.download = `${props.topic}.png`;
link.href = blob;
link.click();
showImageModal(blob);
}
})
.catch((e) => console.log("[Export Image] ", e));
} else {
const link = document.createElement("a");
link.download = `${props.topic}.png`;
link.href = blob;
link.click();
refreshPreview();
}
} catch (error) {
showToast(Locale.Download.Failed);
}
};
const refreshPreview = () => {
const dom = previewRef.current;
if (dom) {
dom.innerHTML = dom.innerHTML; // Refresh the content of the preview by resetting its HTML for fix a bug glitching
}
};
return (
@@ -437,7 +547,7 @@ export function ImagePreviewer(props: {
</div>
<div>
<div className={styles["main-title"]}>ChatGPT Next Web</div>
<div className={styles["main-title"]}>NextChat</div>
<div className={styles["sub-title"]}>
github.com/Yidadaa/ChatGPT-Next-Web
</div>
@@ -449,16 +559,16 @@ export function ImagePreviewer(props: {
</div>
<div>
<div className={styles["chat-info-item"]}>
Model: {mask.modelConfig.model}
{Locale.Exporter.Model}: {mask.modelConfig.model}
</div>
<div className={styles["chat-info-item"]}>
Messages: {props.messages.length}
{Locale.Exporter.Messages}: {props.messages.length}
</div>
<div className={styles["chat-info-item"]}>
Topic: {session.topic}
{Locale.Exporter.Topic}: {session.topic}
</div>
<div className={styles["chat-info-item"]}>
Time:{" "}
{Locale.Exporter.Time}:{" "}
{new Date(
props.messages.at(-1)?.date ?? Date.now(),
).toLocaleString()}
@@ -479,10 +589,37 @@ export function ImagePreviewer(props: {
<div className={styles["body"]}>
<Markdown
content={m.content}
content={getMessageTextContent(m)}
fontSize={config.fontSize}
defaultShow
/>
{getMessageImages(m).length == 1 && (
<img
key={i}
src={getMessageImages(m)[0]}
alt="message"
className={styles["message-image"]}
/>
)}
{getMessageImages(m).length > 1 && (
<div
className={styles["message-images"]}
style={
{
"--image-count": getMessageImages(m).length,
} as React.CSSProperties
}
>
{getMessageImages(m).map((src, i) => (
<img
key={i}
src={src}
alt="message"
className={styles["message-image-multi"]}
/>
))}
</div>
)}
</div>
</div>
);
@@ -501,8 +638,10 @@ export function MarkdownPreviewer(props: {
props.messages
.map((m) => {
return m.role === "user"
? `## ${Locale.Export.MessageFromYou}:\n${m.content}`
: `## ${Locale.Export.MessageFromChatGPT}:\n${m.content.trim()}`;
? `## ${Locale.Export.MessageFromYou}:\n${getMessageTextContent(m)}`
: `## ${Locale.Export.MessageFromChatGPT}:\n${getMessageTextContent(
m,
).trim()}`;
})
.join("\n\n");
@@ -512,12 +651,12 @@ export function MarkdownPreviewer(props: {
const download = () => {
downloadAs(mdText, `${props.topic}.md`);
};
return (
<>
<PreviewActions
copy={copy}
download={download}
showCopy={true}
messages={props.messages}
/>
<div className="markdown-body">
@@ -526,3 +665,44 @@ export function MarkdownPreviewer(props: {
</>
);
}
export function JsonPreviewer(props: {
messages: ChatMessage[];
topic: string;
}) {
const msgs = {
messages: [
{
role: "system",
content: `${Locale.FineTuned.Sysmessage} ${props.topic}`,
},
...props.messages.map((m) => ({
role: m.role,
content: m.content,
})),
],
};
const mdText = "```json\n" + JSON.stringify(msgs, null, 2) + "\n```";
const minifiedJson = JSON.stringify(msgs);
const copy = () => {
copyToClipboard(minifiedJson);
};
const download = () => {
downloadAs(JSON.stringify(msgs), `${props.topic}.json`);
};
return (
<>
<PreviewActions
copy={copy}
download={download}
showCopy={false}
messages={props.messages}
/>
<div className="markdown-body" onClick={copy}>
<Markdown content={mdText} />
</div>
</>
);
}

View File

@@ -6,7 +6,7 @@
color: var(--black);
background-color: var(--white);
min-width: 600px;
min-height: 480px;
min-height: 370px;
max-width: 1200px;
display: flex;
@@ -61,24 +61,36 @@
}
}
}
&:hover,
&:active {
.sidebar-drag {
background-color: rgba($color: #000000, $alpha: 0.01);
svg {
opacity: 0.2;
}
}
}
}
.sidebar-drag {
$width: 10px;
$width: 14px;
position: absolute;
top: 0;
right: 0;
height: 100%;
width: $width;
background-color: var(--black);
background-color: rgba($color: #000000, $alpha: 0);
cursor: ew-resize;
opacity: 0;
transition: all ease 0.3s;
display: flex;
align-items: center;
&:hover,
&:active {
opacity: 0.2;
svg {
opacity: 0;
margin-left: -2px;
}
}
@@ -162,6 +174,7 @@
user-select: none;
border: 2px solid transparent;
position: relative;
content-visibility: auto;
}
.chat-item:hover {
@@ -185,7 +198,7 @@
.chat-item-delete {
position: absolute;
top: 10px;
top: 0;
right: 0;
transition: all ease 0.3s;
opacity: 0;
@@ -194,7 +207,7 @@
.chat-item:hover > .chat-item-delete {
opacity: 0.5;
transform: translateX(-10px);
transform: translateX(-4px);
}
.chat-item:hover > .chat-item-delete:hover {
@@ -283,15 +296,6 @@
}
}
.chat-item-delete {
top: 15px;
}
.chat-item:hover > .chat-item-delete {
opacity: 0.5;
right: 5px;
}
.sidebar-tail {
flex-direction: column-reverse;
align-items: center;
@@ -322,243 +326,6 @@
margin-right: 15px;
}
.chat {
display: flex;
flex-direction: column;
position: relative;
height: 100%;
}
.chat-body {
flex: 1;
overflow: auto;
padding: 20px;
padding-bottom: 40px;
position: relative;
overscroll-behavior: none;
}
.chat-body-title {
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
.chat-message {
display: flex;
flex-direction: row;
&:last-child {
animation: slide-in ease 0.3s;
}
}
.chat-message-user {
display: flex;
flex-direction: row-reverse;
}
.chat-message-container {
max-width: var(--message-max-width);
display: flex;
flex-direction: column;
align-items: flex-start;
&:hover {
.chat-message-top-actions {
opacity: 1;
transform: translateX(10px);
pointer-events: all;
}
}
}
.chat-message-user > .chat-message-container {
align-items: flex-end;
}
.chat-message-avatar {
margin-top: 20px;
}
.chat-message-status {
font-size: 12px;
color: #aaa;
line-height: 1.5;
margin-top: 5px;
}
.chat-message-item {
box-sizing: border-box;
max-width: 100%;
margin-top: 10px;
border-radius: 10px;
background-color: rgba(0, 0, 0, 0.05);
padding: 10px;
font-size: 14px;
user-select: text;
word-break: break-word;
border: var(--border-in-light);
position: relative;
}
.chat-message-top-actions {
min-width: 120px;
font-size: 12px;
position: absolute;
right: 20px;
top: -26px;
left: 30px;
transition: all ease 0.3s;
opacity: 0;
pointer-events: none;
display: flex;
flex-direction: row-reverse;
.chat-message-top-action {
opacity: 0.5;
color: var(--black);
white-space: nowrap;
cursor: pointer;
&:hover {
opacity: 1;
}
&:not(:first-child) {
margin-right: 10px;
}
}
}
.chat-message-user > .chat-message-container > .chat-message-item {
background-color: var(--second);
}
.chat-message-actions {
display: flex;
flex-direction: row-reverse;
width: 100%;
padding-top: 5px;
box-sizing: border-box;
font-size: 12px;
}
.chat-message-action-date {
color: #aaa;
}
.chat-input-panel {
position: relative;
width: 100%;
padding: 20px;
padding-top: 10px;
box-sizing: border-box;
flex-direction: column;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
border-top: var(--border-in-light);
box-shadow: var(--card-shadow);
}
@mixin single-line {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.prompt-hints {
min-height: 20px;
width: 100%;
max-height: 50vh;
overflow: auto;
display: flex;
flex-direction: column-reverse;
background-color: var(--white);
border: var(--border-in-light);
border-radius: 10px;
margin-bottom: 10px;
box-shadow: var(--shadow);
.prompt-hint {
color: var(--black);
padding: 6px 10px;
animation: slide-in ease 0.3s;
cursor: pointer;
transition: all ease 0.3s;
border: transparent 1px solid;
margin: 4px;
border-radius: 8px;
&:not(:last-child) {
margin-top: 0;
}
.hint-title {
font-size: 12px;
font-weight: bolder;
@include single-line();
}
.hint-content {
font-size: 12px;
@include single-line();
}
&-selected,
&:hover {
border-color: var(--primary);
}
}
}
.chat-input-panel-inner {
display: flex;
flex: 1;
}
.chat-input {
height: 100%;
width: 100%;
border-radius: 10px;
border: var(--border-in-light);
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.03);
background-color: var(--white);
color: var(--black);
font-family: inherit;
padding: 10px 90px 10px 14px;
resize: none;
outline: none;
}
.chat-input:focus {
border: 1px solid var(--primary);
}
.chat-input-send {
background-color: var(--primary);
color: white;
position: absolute;
right: 30px;
bottom: 32px;
}
@media only screen and (max-width: 600px) {
.chat-input {
font-size: 16px;
}
.chat-input-send {
bottom: 30px;
}
}
.loading-content {
display: flex;
flex-direction: column;
@@ -567,3 +334,7 @@
height: 100%;
width: 100%;
}
.rtl-screen {
direction: rtl;
}

View File

@@ -12,9 +12,11 @@ import LoadingIcon from "../icons/three-dots.svg";
import { getCSSVar, useMobileScreen } from "../utils";
import dynamic from "next/dynamic";
import { Path, SlotID } from "../constant";
import { ModelProvider, Path, SlotID } from "../constant";
import { ErrorBoundary } from "./error";
import { getISOLang, getLang } from "../locales";
import {
HashRouter as Router,
Routes,
@@ -25,6 +27,9 @@ import { SideBar } from "./sidebar";
import { useAppConfig } from "../store/config";
import { AuthPage } from "./auth";
import { getClientConfig } from "../config/client";
import { ClientApi } from "../client/api";
import { useAccessStore } from "../store";
import { identifyDefaultClaudeModel } from "../utils/checkers";
export function Loading(props: { noLogo?: boolean }) {
return (
@@ -82,6 +87,17 @@ export function useSwitchTheme() {
}, [config.theme]);
}
function useHtmlLang() {
useEffect(() => {
const lang = getISOLang();
const htmlLang = document.documentElement.lang;
if (lang !== htmlLang) {
document.documentElement.lang = lang;
}
}, []);
}
const useHasHydrated = () => {
const [hasHydrated, setHasHydrated] = useState<boolean>(false);
@@ -101,7 +117,9 @@ const loadAsyncGoogleFont = () => {
linkEl.rel = "stylesheet";
linkEl.href =
googleFontUrl +
"/css2?family=Noto+Sans+SC:wght@300;400;700;900&display=swap";
"/css2?family=" +
encodeURIComponent("Noto Sans:wght@300;400;700;900") +
"&display=swap";
document.head.appendChild(linkEl);
};
@@ -111,6 +129,8 @@ function Screen() {
const isHome = location.pathname === Path.Home;
const isAuth = location.pathname === Path.Auth;
const isMobileScreen = useMobileScreen();
const shouldTightBorder =
getClientConfig()?.isApp || (config.tightBorder && !isMobileScreen);
useEffect(() => {
loadAsyncGoogleFont();
@@ -120,10 +140,8 @@ function Screen() {
<div
className={
styles.container +
` ${
config.tightBorder && !isMobileScreen
? styles["tight-container"]
: styles.container
` ${shouldTightBorder ? styles["tight-container"] : styles.container} ${
getLang() === "ar" ? styles["rtl-screen"] : ""
}`
}
>
@@ -150,11 +168,34 @@ function Screen() {
);
}
export function useLoadData() {
const config = useAppConfig();
var api: ClientApi;
if (config.modelConfig.model.startsWith("gemini")) {
api = new ClientApi(ModelProvider.GeminiPro);
} else if (identifyDefaultClaudeModel(config.modelConfig.model)) {
api = new ClientApi(ModelProvider.Claude);
} else {
api = new ClientApi(ModelProvider.GPT);
}
useEffect(() => {
(async () => {
const models = await api.llm.models();
config.mergeModels(models);
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}
export function Home() {
useSwitchTheme();
useLoadData();
useHtmlLang();
useEffect(() => {
console.log("[Config] got config from build time", getClientConfig());
useAccessStore.getState().fetch();
}, []);
if (!useHasHydrated()) {

View File

@@ -1,12 +1,13 @@
.input-range {
border: var(--border-in-light);
border-radius: 10px;
padding: 5px 15px 5px 10px;
padding: 5px 10px 5px 10px;
font-size: 12px;
display: flex;
justify-content: space-between;
max-width: 40%;
input[type="range"] {
max-width: calc(100% - 50px);
max-width: calc(100% - 34px);
}
}

View File

@@ -5,25 +5,28 @@ import RemarkBreaks from "remark-breaks";
import RehypeKatex from "rehype-katex";
import RemarkGfm from "remark-gfm";
import RehypeHighlight from "rehype-highlight";
import { useRef, useState, RefObject, useEffect } from "react";
import { useRef, useState, RefObject, useEffect, useMemo } from "react";
import { copyToClipboard } from "../utils";
import mermaid from "mermaid";
import LoadingIcon from "../icons/three-dots.svg";
import React from "react";
import { useThrottledCallback } from "use-debounce";
import { useDebouncedCallback } from "use-debounce";
import { showImageModal } from "./ui-lib";
export function Mermaid(props: { code: string; onError: () => void }) {
export function Mermaid(props: { code: string }) {
const ref = useRef<HTMLDivElement>(null);
const [hasError, setHasError] = useState(false);
useEffect(() => {
if (props.code && ref.current) {
mermaid
.run({
nodes: [ref.current],
suppressErrors: true,
})
.catch((e) => {
props.onError();
setHasError(true);
console.error("[Mermaid] ", e.message);
});
}
@@ -35,17 +38,20 @@ export function Mermaid(props: { code: string; onError: () => void }) {
if (!svg) return;
const text = new XMLSerializer().serializeToString(svg);
const blob = new Blob([text], { type: "image/svg+xml" });
const url = URL.createObjectURL(blob);
const win = window.open(url);
if (win) {
win.onload = () => URL.revokeObjectURL(url);
}
showImageModal(URL.createObjectURL(blob));
}
if (hasError) {
return null;
}
return (
<div
className="no-dark"
style={{ cursor: "pointer", overflow: "auto" }}
className="no-dark mermaid"
style={{
cursor: "pointer",
overflow: "auto",
}}
ref={ref}
onClick={() => viewSvgInNewWindow()}
>
@@ -56,37 +62,83 @@ export function Mermaid(props: { code: string; onError: () => void }) {
export function PreCode(props: { children: any }) {
const ref = useRef<HTMLPreElement>(null);
const refText = ref.current?.innerText;
const [mermaidCode, setMermaidCode] = useState("");
useEffect(() => {
const renderMermaid = useDebouncedCallback(() => {
if (!ref.current) return;
const mermaidDom = ref.current.querySelector("code.language-mermaid");
if (mermaidDom) {
setMermaidCode((mermaidDom as HTMLElement).innerText);
}
}, [props.children]);
}, 600);
if (mermaidCode) {
return <Mermaid code={mermaidCode} onError={() => setMermaidCode("")} />;
}
useEffect(() => {
setTimeout(renderMermaid, 1);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [refText]);
return (
<pre ref={ref}>
<span
className="copy-code-button"
onClick={() => {
if (ref.current) {
const code = ref.current.innerText;
copyToClipboard(code);
}
}}
></span>
{props.children}
</pre>
<>
{mermaidCode.length > 0 && (
<Mermaid code={mermaidCode} key={mermaidCode} />
)}
<pre ref={ref}>
<span
className="copy-code-button"
onClick={() => {
if (ref.current) {
const code = ref.current.innerText;
copyToClipboard(code);
}
}}
></span>
{props.children}
</pre>
</>
);
}
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;
return text.replace(
pattern,
(match, codeBlock, squareBracket, roundBracket) => {
if (codeBlock) {
return codeBlock;
} else if (squareBracket) {
return `$$${squareBracket}$$`;
} else if (roundBracket) {
return `$${roundBracket}$`;
}
return match;
},
);
}
function _MarkDownContent(props: { content: string }) {
const escapedContent = useMemo(() => {
return escapeBrackets(escapeDollarNumber(props.content));
}, [props.content]);
return (
<ReactMarkdown
remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}
@@ -102,6 +154,7 @@ function _MarkDownContent(props: { content: string }) {
]}
components={{
pre: PreCode,
p: (pProps) => <p {...pProps} dir="auto" />,
a: (aProps) => {
const href = aProps.href || "";
const isInternal = /^\/#/i.test(href);
@@ -110,7 +163,7 @@ function _MarkDownContent(props: { content: string }) {
},
}}
>
{props.content}
{escapedContent}
</ReactMarkdown>
);
}
@@ -124,72 +177,27 @@ export function Markdown(
fontSize?: number;
parentRef?: RefObject<HTMLDivElement>;
defaultShow?: boolean;
className?: string;
} & React.DOMAttributes<HTMLDivElement>,
) {
const mdRef = useRef<HTMLDivElement>(null);
const renderedHeight = useRef(0);
const renderedWidth = useRef(0);
const inView = useRef(!!props.defaultShow);
const [_, triggerRender] = useState(0);
const checkInView = useThrottledCallback(
() => {
const parent = props.parentRef?.current;
const md = mdRef.current;
if (parent && md && !props.defaultShow) {
const parentBounds = parent.getBoundingClientRect();
const twoScreenHeight = Math.max(500, parentBounds.height * 2);
const mdBounds = md.getBoundingClientRect();
const parentTop = parentBounds.top - twoScreenHeight;
const parentBottom = parentBounds.bottom + twoScreenHeight;
const isOverlap =
Math.max(parentTop, mdBounds.top) <=
Math.min(parentBottom, mdBounds.bottom);
inView.current = isOverlap;
triggerRender(Date.now());
}
if (inView.current && md) {
const rect = md.getBoundingClientRect();
renderedHeight.current = Math.max(renderedHeight.current, rect.height);
renderedWidth.current = Math.max(renderedWidth.current, rect.width);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
},
300,
{
leading: true,
trailing: true,
},
);
useEffect(() => {
props.parentRef?.current?.addEventListener("scroll", checkInView);
checkInView();
return () =>
props.parentRef?.current?.removeEventListener("scroll", checkInView);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const getSize = (x: number) => (!inView.current && x > 0 ? x : "auto");
return (
<div
className="markdown-body"
className={`markdown-body ${props.className}`}
style={{
fontSize: `${props.fontSize ?? 14}px`,
height: getSize(renderedHeight.current),
width: getSize(renderedWidth.current),
}}
ref={mdRef}
onContextMenu={props.onContextMenu}
onDoubleClickCapture={props.onDoubleClickCapture}
dir="auto"
>
{inView.current &&
(props.loading ? (
<LoadingIcon />
) : (
<MarkdownContent content={props.content} />
))}
{props.loading ? (
<LoadingIcon />
) : (
<MarkdownContent content={props.content} />
)}
</div>
);
}

View File

@@ -4,6 +4,10 @@
display: flex;
flex-direction: column;
div:not(.no-dark) > svg {
filter: invert(0.5);
}
.mask-page-body {
padding: 20px;
overflow-y: auto;

View File

@@ -1,5 +1,4 @@
import { IconButton } from "./button";
import { ErrorBoundary } from "./error";
import styles from "./mask.module.scss";
@@ -11,28 +10,66 @@ import CloseIcon from "../icons/close.svg";
import DeleteIcon from "../icons/delete.svg";
import EyeIcon from "../icons/eye.svg";
import CopyIcon from "../icons/copy.svg";
import DragIcon from "../icons/drag.svg";
import { DEFAULT_MASK_AVATAR, Mask, useMaskStore } from "../store/mask";
import { ChatMessage, ModelConfig, useAppConfig, useChatStore } from "../store";
import { ROLES } from "../client/api";
import { Input, List, ListItem, Modal, Popover, Select } from "./ui-lib";
import {
ChatMessage,
createMessage,
ModelConfig,
ModelType,
useAppConfig,
useChatStore,
} from "../store";
import { MultimodalContent, ROLES } from "../client/api";
import {
Input,
List,
ListItem,
Modal,
Popover,
Select,
showConfirm,
} from "./ui-lib";
import { Avatar, AvatarPicker } from "./emoji";
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 { downloadAs, readFromFile } from "../utils";
import {
copyToClipboard,
downloadAs,
getMessageImages,
readFromFile,
} from "../utils";
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,
Draggable,
OnDragEndResponder,
} from "@hello-pangea/dnd";
import { getMessageTextContent } from "../utils";
import useMobileScreen from "@/app/hooks/useMobileScreen";
export function MaskAvatar(props: { mask: Mask }) {
return props.mask.avatar !== DEFAULT_MASK_AVATAR ? (
<Avatar avatar={props.mask.avatar} />
// drag and drop helper function
function reorder<T>(list: T[], startIndex: number, endIndex: number): T[] {
const result = [...list];
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
return result;
}
export function MaskAvatar(props: { avatar: string; model?: ModelType }) {
return props.avatar !== DEFAULT_MASK_AVATAR ? (
<Avatar avatar={props.avatar} />
) : (
<Avatar model={props.mask.modelConfig.model} />
<Avatar model={props.model} />
);
}
@@ -57,6 +94,11 @@ export function MaskConfig(props: {
});
};
const copyMaskLink = () => {
const maskLink = `${location.protocol}//${location.host}/#${Path.NewChat}?mask=${props.mask.id}`;
copyToClipboard(maskLink);
};
const globalConfig = useAppConfig();
return (
@@ -88,7 +130,10 @@ export function MaskConfig(props: {
onClick={() => setShowPicker(true)}
style={{ cursor: "pointer" }}
>
<MaskAvatar mask={props.mask} />
<MaskAvatar
avatar={props.mask.avatar}
model={props.mask.modelConfig.model}
/>
</div>
</Popover>
</ListItem>
@@ -117,6 +162,20 @@ export function MaskConfig(props: {
}}
></input>
</ListItem>
{!props.shouldSyncFromGlobal ? (
<ListItem
title={Locale.Mask.Config.Share.Title}
subTitle={Locale.Mask.Config.Share.SubTitle}
>
<IconButton
icon={<CopyIcon />}
text={Locale.Mask.Config.Share.Action}
onClick={copyMaskLink}
/>
</ListItem>
) : null}
{props.shouldSyncFromGlobal ? (
<ListItem
title={Locale.Mask.Config.Sync.Title}
@@ -125,15 +184,20 @@ export function MaskConfig(props: {
<input
type="checkbox"
checked={props.mask.syncGlobalConfig}
onChange={(e) => {
onChange={async (e) => {
const checked = e.currentTarget.checked;
if (
e.currentTarget.checked &&
confirm(Locale.Mask.Config.Sync.Confirm)
checked &&
(await showConfirm(Locale.Mask.Config.Sync.Confirm))
) {
props.updateMask((mask) => {
mask.syncGlobalConfig = e.currentTarget.checked;
mask.syncGlobalConfig = checked;
mask.modelConfig = { ...globalConfig.modelConfig };
});
} else if (!checked) {
props.updateMask((mask) => {
mask.syncGlobalConfig = checked;
});
}
}}
></input>
@@ -153,6 +217,7 @@ export function MaskConfig(props: {
}
function ContextPromptItem(props: {
index: number;
prompt: ChatMessage;
update: (prompt: ChatMessage) => void;
remove: () => void;
@@ -162,25 +227,30 @@ function ContextPromptItem(props: {
return (
<div className={chatStyle["context-prompt-row"]}>
{!focusingInput && (
<Select
value={props.prompt.role}
className={chatStyle["context-role"]}
onChange={(e) =>
props.update({
...props.prompt,
role: e.target.value as any,
})
}
>
{ROLES.map((r) => (
<option key={r} value={r}>
{r}
</option>
))}
</Select>
<>
<div className={chatStyle["context-drag"]}>
<DragIcon />
</div>
<Select
value={props.prompt.role}
className={chatStyle["context-role"]}
onChange={(e) =>
props.update({
...props.prompt,
role: e.target.value as any,
})
}
>
{ROLES.map((r) => (
<option key={r} value={r}>
{r}
</option>
))}
</Select>
</>
)}
<Input
value={props.prompt.content}
value={getMessageTextContent(props.prompt)}
type="text"
className={chatStyle["context-content"]}
rows={focusingInput ? 5 : 1}
@@ -216,8 +286,8 @@ export function ContextPrompts(props: {
}) {
const context = props.context;
const addContextPrompt = (prompt: ChatMessage) => {
props.updateContext((context) => context.push(prompt));
const addContextPrompt = (prompt: ChatMessage, i: number) => {
props.updateContext((context) => context.splice(i, 0, prompt));
};
const removeContextPrompt = (i: number) => {
@@ -225,48 +295,125 @@ export function ContextPrompts(props: {
};
const updateContextPrompt = (i: number, prompt: ChatMessage) => {
props.updateContext((context) => (context[i] = prompt));
props.updateContext((context) => {
const images = getMessageImages(context[i]);
context[i] = prompt;
if (images.length > 0) {
const text = getMessageTextContent(context[i]);
const newContext: MultimodalContent[] = [{ type: "text", text }];
for (const img of images) {
newContext.push({ type: "image_url", image_url: { url: img } });
}
context[i].content = newContext;
}
});
};
const onDragEnd: OnDragEndResponder = (result) => {
if (!result.destination) {
return;
}
const newContext = reorder(
context,
result.source.index,
result.destination.index,
);
props.updateContext((context) => {
context.splice(0, context.length, ...newContext);
});
};
return (
<>
<div className={chatStyle["context-prompt"]} style={{ marginBottom: 20 }}>
{context.map((c, i) => (
<ContextPromptItem
key={i}
prompt={c}
update={(prompt) => updateContextPrompt(i, prompt)}
remove={() => removeContextPrompt(i)}
/>
))}
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="context-prompt-list">
{(provided) => (
<div ref={provided.innerRef} {...provided.droppableProps}>
{context.map((c, i) => (
<Draggable
draggableId={c.id || i.toString()}
index={i}
key={c.id}
>
{(provided) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<ContextPromptItem
index={i}
prompt={c}
update={(prompt) => updateContextPrompt(i, prompt)}
remove={() => removeContextPrompt(i)}
/>
<div
className={chatStyle["context-prompt-insert"]}
onClick={() => {
addContextPrompt(
createMessage({
role: "user",
content: "",
date: new Date().toLocaleString(),
}),
i + 1,
);
}}
>
<AddIcon />
</div>
</div>
)}
</Draggable>
))}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
<div className={chatStyle["context-prompt-row"]}>
<IconButton
icon={<AddIcon />}
text={Locale.Context.Add}
bordered
className={chatStyle["context-prompt-button"]}
onClick={() =>
addContextPrompt({
role: "user",
content: "",
date: "",
})
}
/>
</div>
{props.context.length === 0 && (
<div className={chatStyle["context-prompt-row"]}>
<IconButton
icon={<AddIcon />}
text={Locale.Context.Add}
bordered
className={chatStyle["context-prompt-button"]}
onClick={() =>
addContextPrompt(
createMessage({
role: "user",
content: "",
date: "",
}),
props.context.length,
)
}
/>
</div>
)}
</div>
</>
);
}
export function MaskPage() {
export function MaskPage(props: { className?: string }) {
const navigate = useNavigate();
const maskStore = useMaskStore();
const chatStore = useChatStore();
const [filterLang, setFilterLang] = useState<Lang>();
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 allMasks = maskStore
.getAll()
@@ -276,24 +423,26 @@ export function MaskPage() {
const [searchText, setSearchText] = useState("");
const masks = searchText.length > 0 ? searchMasks : allMasks;
// simple search, will refactor later
// refactored already, now it accurate
const onSearch = (text: string) => {
setSearchText(text);
if (text.length > 0) {
const result = allMasks.filter((m) => m.name.includes(text));
const result = allMasks.filter((m) =>
m.name.toLowerCase().includes(text.toLowerCase()),
);
setSearchMasks(result);
} else {
setSearchMasks(allMasks);
}
};
const [editingMaskId, setEditingMaskId] = useState<number | undefined>();
const [editingMaskId, setEditingMaskId] = useState<string | undefined>();
const editingMask =
maskStore.get(editingMaskId) ?? BUILTIN_MASK_STORE.get(editingMaskId);
const closeMaskModal = () => setEditingMaskId(undefined);
const downloadAll = () => {
downloadAs(JSON.stringify(masks), FileName.Masks);
downloadAs(JSON.stringify(masks.filter((v) => !v.builtin)), FileName.Masks);
};
const importFromFile = () => {
@@ -317,8 +466,13 @@ export function MaskPage() {
};
return (
<ErrorBoundary>
<div className={styles["mask-page"]}>
<>
<div
className={`
${styles["mask-page"]}
${props.className}
`}
>
<div className="window-header">
<div className="window-header-title">
<div className="window-header-main-title">
@@ -335,11 +489,13 @@ export function MaskPage() {
icon={<DownloadIcon />}
bordered
onClick={downloadAll}
text={Locale.UI.Export}
/>
</div>
<div className="window-action-button">
<IconButton
icon={<UploadIcon />}
text={Locale.UI.Import}
bordered
onClick={() => importFromFile()}
/>
@@ -402,7 +558,7 @@ export function MaskPage() {
<div className={styles["mask-item"]} key={m.id}>
<div className={styles["mask-header"]}>
<div className={styles["mask-icon"]}>
<MaskAvatar mask={m} />
<MaskAvatar avatar={m.avatar} model={m.modelConfig.model} />
</div>
<div className={styles["mask-title"]}>
<div className={styles["mask-name"]}>{m.name}</div>
@@ -439,8 +595,8 @@ export function MaskPage() {
<IconButton
icon={<DeleteIcon />}
text={Locale.Mask.Item.Delete}
onClick={() => {
if (confirm(Locale.Mask.Item.DeleteConfirm)) {
onClick={async () => {
if (await showConfirm(Locale.Mask.Item.DeleteConfirm)) {
maskStore.delete(m.id);
}
}}
@@ -487,13 +643,13 @@ export function MaskPage() {
<MaskConfig
mask={editingMask}
updateMask={(updater) =>
maskStore.update(editingMaskId!, updater)
maskStore.updateMask(editingMaskId!, updater)
}
readonly={editingMask.builtin}
/>
</Modal>
</div>
)}
</ErrorBoundary>
</>
);
}

View File

@@ -58,8 +58,8 @@
}
.body {
flex-grow: 1;
max-width: calc(100% - 40px);
flex: 1;
max-width: calc(100% - 80px);
.date {
font-size: 12px;
@@ -71,6 +71,12 @@
font-size: 12px;
}
}
.checkbox {
display: flex;
justify-content: flex-end;
flex: 1;
}
}
}
}

View File

@@ -1,4 +1,4 @@
import { useEffect, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { ChatMessage, useAppConfig, useChatStore } from "../store";
import { Updater } from "../typing";
import { IconButton } from "./button";
@@ -7,6 +7,7 @@ import { MaskAvatar } from "./mask";
import Locale from "../locales";
import styles from "./message-selector.module.scss";
import { getMessageTextContent } from "../utils";
function useShiftRange() {
const [startIndex, setStartIndex] = useState<number>();
@@ -51,9 +52,9 @@ function useShiftRange() {
}
export function useMessageSelector() {
const [selection, setSelection] = useState(new Set<number>());
const updateSelection: Updater<Set<number>> = (updater) => {
const newSelection = new Set<number>(selection);
const [selection, setSelection] = useState(new Set<string>());
const updateSelection: Updater<Set<string>> = (updater) => {
const newSelection = new Set<string>(selection);
updater(newSelection);
setSelection(newSelection);
};
@@ -65,33 +66,47 @@ export function useMessageSelector() {
}
export function MessageSelector(props: {
selection: Set<number>;
updateSelection: Updater<Set<number>>;
selection: Set<string>;
updateSelection: Updater<Set<string>>;
defaultSelectAll?: boolean;
onSelected?: (messages: ChatMessage[]) => void;
}) {
const chatStore = useChatStore();
const session = chatStore.currentSession();
const isValid = (m: ChatMessage) => m.content && !m.isError && !m.streaming;
const messages = session.messages.filter(
(m, i) =>
m.id && // message must have id
isValid(m) &&
(i >= session.messages.length - 1 || isValid(session.messages[i + 1])),
const allMessages = useMemo(() => {
let startIndex = Math.max(0, session.clearContextIndex ?? 0);
if (startIndex === session.messages.length - 1) {
startIndex = 0;
}
return session.messages.slice(startIndex);
}, [session.messages, session.clearContextIndex]);
const messages = useMemo(
() =>
allMessages.filter(
(m, i) =>
m.id && // message must have id
isValid(m) &&
(i >= allMessages.length - 1 || isValid(allMessages[i + 1])),
),
[allMessages],
);
const messageCount = messages.length;
const config = useAppConfig();
const [searchInput, setSearchInput] = useState("");
const [searchIds, setSearchIds] = useState(new Set<number>());
const isInSearchResult = (id: number) => {
const [searchIds, setSearchIds] = useState(new Set<string>());
const isInSearchResult = (id: string) => {
return searchInput.length === 0 || searchIds.has(id);
};
const doSearch = (text: string) => {
const searchResults = new Set<number>();
const searchResults = new Set<string>();
if (text.length > 0) {
messages.forEach((m) =>
m.content.includes(text) ? searchResults.add(m.id!) : null,
getMessageTextContent(m).includes(text)
? searchResults.add(m.id!)
: null,
);
}
setSearchIds(searchResults);
@@ -176,6 +191,8 @@ export function MessageSelector(props: {
<div className={styles["messages"]}>
{messages.map((m, i) => {
if (!isInSearchResult(m.id!)) return null;
const id = m.id ?? i;
const isSelected = props.selection.has(id);
return (
<div
@@ -185,7 +202,6 @@ export function MessageSelector(props: {
key={i}
onClick={() => {
props.updateSelection((selection) => {
const id = m.id ?? i;
selection.has(id) ? selection.delete(id) : selection.add(id);
});
onClickIndex(i);
@@ -195,7 +211,10 @@ export function MessageSelector(props: {
{m.role === "user" ? (
<Avatar avatar={config.avatar}></Avatar>
) : (
<MaskAvatar mask={session.mask} />
<MaskAvatar
avatar={session.mask.avatar}
model={m.model || session.mask.modelConfig.model}
/>
)}
</div>
<div className={styles["body"]}>
@@ -203,9 +222,13 @@ export function MessageSelector(props: {
{new Date(m.date).toLocaleString()}
</div>
<div className={`${styles["content"]} one-line`}>
{m.content}
{getMessageTextContent(m)}
</div>
</div>
<div className={styles["checkbox"]}>
<input type="checkbox" checked={isSelected} readOnly></input>
</div>
</div>
);
})}

View File

@@ -1,13 +1,16 @@
import { ALL_MODELS, ModalConfigValidator, ModelConfig } from "../store";
import { ModalConfigValidator, ModelConfig } from "../store";
import Locale from "../locales";
import { InputRange } from "./input-range";
import { List, ListItem, Select } from "./ui-lib";
import { ListItem, Select } from "./ui-lib";
import { useAllModels } from "../utils/hooks";
export function ModelConfigList(props: {
modelConfig: ModelConfig;
updateConfig: (updater: (config: ModelConfig) => void) => void;
}) {
const allModels = useAllModels();
return (
<>
<ListItem title={Locale.Settings.Model}>
@@ -22,11 +25,13 @@ export function ModelConfigList(props: {
);
}}
>
{ALL_MODELS.map((v) => (
<option value={v.name} key={v.name} disabled={!v.available}>
{v.name}
</option>
))}
{allModels
.filter((v) => v.available)
.map((v, i) => (
<option value={v.name} key={i}>
{v.displayName}({v.provider?.providerName})
</option>
))}
</Select>
</ListItem>
<ListItem
@@ -48,14 +53,33 @@ export function ModelConfigList(props: {
}}
></InputRange>
</ListItem>
<ListItem
title={Locale.Settings.TopP.Title}
subTitle={Locale.Settings.TopP.SubTitle}
>
<InputRange
value={(props.modelConfig.top_p ?? 1).toFixed(1)}
min="0"
max="1"
step="0.1"
onChange={(e) => {
props.updateConfig(
(config) =>
(config.top_p = ModalConfigValidator.top_p(
e.currentTarget.valueAsNumber,
)),
);
}}
></InputRange>
</ListItem>
<ListItem
title={Locale.Settings.MaxTokens.Title}
subTitle={Locale.Settings.MaxTokens.SubTitle}
>
<input
type="number"
min={100}
max={32000}
min={1024}
max={512000}
value={props.modelConfig.max_tokens}
onChange={(e) =>
props.updateConfig(
@@ -67,27 +91,84 @@ export function ModelConfigList(props: {
}
></input>
</ListItem>
<ListItem
title={Locale.Settings.PresencePenalty.Title}
subTitle={Locale.Settings.PresencePenalty.SubTitle}
>
<InputRange
value={props.modelConfig.presence_penalty?.toFixed(1)}
min="-2"
max="2"
step="0.1"
onChange={(e) => {
props.updateConfig(
(config) =>
(config.presence_penalty =
ModalConfigValidator.presence_penalty(
e.currentTarget.valueAsNumber,
)),
);
}}
></InputRange>
</ListItem>
{props.modelConfig.model.startsWith("gemini") ? null : (
<>
<ListItem
title={Locale.Settings.PresencePenalty.Title}
subTitle={Locale.Settings.PresencePenalty.SubTitle}
>
<InputRange
value={props.modelConfig.presence_penalty?.toFixed(1)}
min="-2"
max="2"
step="0.1"
onChange={(e) => {
props.updateConfig(
(config) =>
(config.presence_penalty =
ModalConfigValidator.presence_penalty(
e.currentTarget.valueAsNumber,
)),
);
}}
></InputRange>
</ListItem>
<ListItem
title={Locale.Settings.FrequencyPenalty.Title}
subTitle={Locale.Settings.FrequencyPenalty.SubTitle}
>
<InputRange
value={props.modelConfig.frequency_penalty?.toFixed(1)}
min="-2"
max="2"
step="0.1"
onChange={(e) => {
props.updateConfig(
(config) =>
(config.frequency_penalty =
ModalConfigValidator.frequency_penalty(
e.currentTarget.valueAsNumber,
)),
);
}}
></InputRange>
</ListItem>
<ListItem
title={Locale.Settings.InjectSystemPrompts.Title}
subTitle={Locale.Settings.InjectSystemPrompts.SubTitle}
>
<input
type="checkbox"
checked={props.modelConfig.enableInjectSystemPrompts}
onChange={(e) =>
props.updateConfig(
(config) =>
(config.enableInjectSystemPrompts =
e.currentTarget.checked),
)
}
></input>
</ListItem>
<ListItem
title={Locale.Settings.InputTemplate.Title}
subTitle={Locale.Settings.InputTemplate.SubTitle}
>
<input
type="text"
value={props.modelConfig.template}
onChange={(e) =>
props.updateConfig(
(config) => (config.template = e.currentTarget.value),
)
}
></input>
</ListItem>
</>
)}
<ListItem
title={Locale.Settings.HistoryCount.Title}
subTitle={Locale.Settings.HistoryCount.SubTitle}
@@ -96,7 +177,7 @@ export function ModelConfigList(props: {
title={props.modelConfig.historyMessageCount.toString()}
value={props.modelConfig.historyMessageCount}
min="0"
max="32"
max="64"
step="1"
onChange={(e) =>
props.updateConfig(

View File

@@ -8,6 +8,10 @@
justify-content: center;
flex-direction: column;
div:not(.no-dark) > svg {
filter: invert(0.5);
}
.mask-header {
display: flex;
justify-content: space-between;

View File

@@ -14,22 +14,17 @@ import Locale from "../locales";
import { useAppConfig, useChatStore } from "../store";
import { MaskAvatar } from "./mask";
import { useCommand } from "../command";
function getIntersectionArea(aRect: DOMRect, bRect: DOMRect) {
const xmin = Math.max(aRect.x, bRect.x);
const xmax = Math.min(aRect.x + aRect.width, bRect.x + bRect.width);
const ymin = Math.max(aRect.y, bRect.y);
const ymax = Math.min(aRect.y + aRect.height, bRect.y + bRect.height);
const width = xmax - xmin;
const height = ymax - ymin;
const intersectionArea = width < 0 || height < 0 ? 0 : width * height;
return intersectionArea;
}
import { showConfirm } from "./ui-lib";
import { BUILTIN_MASK_STORE } from "../masks";
import useMobileScreen from "@/app/hooks/useMobileScreen";
function MaskItem(props: { mask: Mask; onClick?: () => void }) {
return (
<div className={styles["mask"]} onClick={props.onClick}>
<MaskAvatar mask={props.mask} />
<MaskAvatar
avatar={props.mask.avatar}
model={props.mask.modelConfig.model}
/>
<div className={styles["mask-name"] + " one-line"}>{props.mask.name}</div>
</div>
);
@@ -77,7 +72,7 @@ function useMaskGroup(masks: Mask[]) {
return groups;
}
export function NewChat() {
export function NewChat(props: { className?: string }) {
const chatStore = useChatStore();
const maskStore = useMaskStore();
@@ -92,14 +87,16 @@ export function NewChat() {
const { state } = useLocation();
const startChat = (mask?: Mask) => {
chatStore.newSession(mask);
setTimeout(() => navigate(Path.Chat), 1);
setTimeout(() => {
chatStore.newSession(mask);
navigate(Path.Chat);
}, 10);
};
useCommand({
mask: (id) => {
try {
const mask = maskStore.get(parseInt(id));
const mask = maskStore.get(id) ?? BUILTIN_MASK_STORE.get(id);
startChat(mask ?? undefined);
} catch {
console.error("[New Chat] failed to create chat from mask id=", id);
@@ -114,8 +111,15 @@ export function NewChat() {
}
}, [groups]);
const isMobileScreen = useMobileScreen();
return (
<div className={styles["new-chat"]}>
<div
className={`
${styles["new-chat"]}
${props.className}
`}
>
<div className={styles["mask-header"]}>
<IconButton
icon={<LeftIcon />}
@@ -125,8 +129,8 @@ export function NewChat() {
{!state?.fromHome && (
<IconButton
text={Locale.NewChat.NotShow}
onClick={() => {
if (confirm(Locale.NewChat.ConfirmNoShow)) {
onClick={async () => {
if (await showConfirm(Locale.NewChat.ConfirmNoShow)) {
startChat();
config.update(
(config) => (config.dontShowMaskSplashScreen = true),

View File

@@ -5,6 +5,8 @@
.avatar {
cursor: pointer;
position: relative;
z-index: 1;
}
.edit-prompt-modal {

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
import { useEffect, useRef } from "react";
import { useEffect, useRef, useMemo } from "react";
import styles from "./home.module.scss";
@@ -8,14 +8,17 @@ 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 PluginIcon from "../icons/plugin.svg";
import DragIcon from "../icons/drag.svg";
import Locale from "../locales";
import { useAppConfig, useChatStore } from "../store";
import {
DEFAULT_SIDEBAR_WIDTH,
MAX_SIDEBAR_WIDTH,
MIN_SIDEBAR_WIDTH,
NARROW_SIDEBAR_WIDTH,
@@ -24,9 +27,9 @@ import {
} from "../constant";
import { Link, useNavigate } from "react-router-dom";
import { useMobileScreen } from "../utils";
import dynamic from "next/dynamic";
import { showToast } from "./ui-lib";
import { showConfirm, showToast } from "./ui-lib";
import { useDeviceInfo } from "../hooks/useDeviceInfo";
const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, {
loading: () => null,
@@ -37,14 +40,11 @@ function useHotKey() {
useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
if (e.metaKey || e.altKey || e.ctrlKey) {
const n = chatStore.sessions.length;
const limit = (x: number) => (x + n) % n;
const i = chatStore.currentSessionIndex;
if (e.altKey || e.ctrlKey) {
if (e.key === "ArrowUp") {
chatStore.selectSession(limit(i - 1));
chatStore.nextSession(-1);
} else if (e.key === "ArrowDown") {
chatStore.selectSession(limit(i + 1));
chatStore.nextSession(1);
}
}
};
@@ -59,31 +59,57 @@ function useDragSideBar() {
const config = useAppConfig();
const startX = useRef(0);
const startDragWidth = useRef(config.sidebarWidth ?? 300);
const startDragWidth = useRef(config.sidebarWidth ?? DEFAULT_SIDEBAR_WIDTH);
const lastUpdateTime = useRef(Date.now());
const handleMouseMove = useRef((e: MouseEvent) => {
if (Date.now() < lastUpdateTime.current + 50) {
return;
}
lastUpdateTime.current = Date.now();
const d = e.clientX - startX.current;
const nextWidth = limit(startDragWidth.current + d);
config.update((config) => (config.sidebarWidth = nextWidth));
});
const handleMouseUp = useRef(() => {
startDragWidth.current = config.sidebarWidth ?? 300;
window.removeEventListener("mousemove", handleMouseMove.current);
window.removeEventListener("mouseup", handleMouseUp.current);
});
const onDragMouseDown = (e: MouseEvent) => {
startX.current = e.clientX;
window.addEventListener("mousemove", handleMouseMove.current);
window.addEventListener("mouseup", handleMouseUp.current);
const toggleSideBar = () => {
config.update((config) => {
if (config.sidebarWidth < MIN_SIDEBAR_WIDTH) {
config.sidebarWidth = DEFAULT_SIDEBAR_WIDTH;
} else {
config.sidebarWidth = NARROW_SIDEBAR_WIDTH;
}
});
};
const onDragStart = (e: MouseEvent) => {
// Remembers the initial width each time the mouse is pressed
startX.current = e.clientX;
startDragWidth.current = config.sidebarWidth;
const dragStartTime = Date.now();
const handleDragMove = (e: MouseEvent) => {
if (Date.now() < lastUpdateTime.current + 20) {
return;
}
lastUpdateTime.current = Date.now();
const d = e.clientX - startX.current;
const nextWidth = limit(startDragWidth.current + d);
config.update((config) => {
if (nextWidth < MIN_SIDEBAR_WIDTH) {
config.sidebarWidth = NARROW_SIDEBAR_WIDTH;
} else {
config.sidebarWidth = nextWidth;
}
});
};
const handleDragEnd = () => {
// In useRef the data is non-responsive, so `config.sidebarWidth` can't get the dynamic sidebarWidth
window.removeEventListener("pointermove", handleDragMove);
window.removeEventListener("pointerup", handleDragEnd);
// if user click the drag icon, should toggle the sidebar
const shouldFireClick = Date.now() - dragStartTime < 300;
if (shouldFireClick) {
toggleSideBar();
}
};
window.addEventListener("pointermove", handleDragMove);
window.addEventListener("pointerup", handleDragEnd);
};
const isMobileScreen = useMobileScreen();
const shouldNarrow =
!isMobileScreen && config.sidebarWidth < MIN_SIDEBAR_WIDTH;
@@ -91,22 +117,22 @@ function useDragSideBar() {
useEffect(() => {
const barWidth = shouldNarrow
? NARROW_SIDEBAR_WIDTH
: limit(config.sidebarWidth ?? 300);
: limit(config.sidebarWidth ?? DEFAULT_SIDEBAR_WIDTH);
const sideBarWidth = isMobileScreen ? "100vw" : `${barWidth}px`;
document.documentElement.style.setProperty("--sidebar-width", sideBarWidth);
}, [config.sidebarWidth, isMobileScreen, shouldNarrow]);
return {
onDragMouseDown,
onDragStart,
shouldNarrow,
};
}
export function SideBar(props: { className?: string }) {
const chatStore = useChatStore();
const { deviceType, systemInfo } = useDeviceInfo();
// drag side bar
const { onDragMouseDown, shouldNarrow } = useDragSideBar();
const { onDragStart, shouldNarrow } = useDragSideBar();
const navigate = useNavigate();
const config = useAppConfig();
@@ -117,10 +143,15 @@ export function SideBar(props: { className?: string }) {
className={`${styles.sidebar} ${props.className} ${
shouldNarrow && styles["narrow-sidebar"]
}`}
style={{
// #3016 disable transition on ios mobile screen
transition:
deviceType === "mobile" && systemInfo === "iOS" ? "none" : undefined,
}}
>
<div className={styles["sidebar-header"]} data-tauri-drag-region>
<div className={styles["sidebar-title"]} data-tauri-drag-region>
ChatGPT Next
NextChat
</div>
<div className={styles["sidebar-sub-title"]}>
Build your own AI assistant.
@@ -135,7 +166,13 @@ export function SideBar(props: { className?: string }) {
icon={<MaskIcon />}
text={shouldNarrow ? undefined : Locale.Mask.Name}
className={styles["sidebar-bar-button"]}
onClick={() => navigate(Path.NewChat, { state: { fromHome: true } })}
onClick={() => {
if (config.dontShowMaskSplashScreen !== true) {
navigate(Path.NewChat, { state: { fromHome: true } });
} else {
navigate(Path.Masks, { state: { fromHome: true } });
}
}}
shadow
/>
<IconButton
@@ -162,9 +199,9 @@ export function SideBar(props: { className?: string }) {
<div className={styles["sidebar-actions"]}>
<div className={styles["sidebar-action"] + " " + styles.mobile}>
<IconButton
icon={<CloseIcon />}
onClick={() => {
if (confirm(Locale.Home.DeleteChat)) {
icon={<DeleteIcon />}
onClick={async () => {
if (await showConfirm(Locale.Home.DeleteChat)) {
chatStore.deleteSession(chatStore.currentSessionIndex);
}
}}
@@ -176,7 +213,7 @@ export function SideBar(props: { className?: string }) {
</Link>
</div>
<div className={styles["sidebar-action"]}>
<a href={REPO_URL} target="_blank">
<a href={REPO_URL} target="_blank" rel="noopener noreferrer">
<IconButton icon={<GithubIcon />} shadow />
</a>
</div>
@@ -200,8 +237,10 @@ export function SideBar(props: { className?: string }) {
<div
className={styles["sidebar-drag"]}
onMouseDown={(e) => onDragMouseDown(e as any)}
></div>
onPointerDown={(e) => onDragStart(e as any)}
>
<DragIcon />
</div>
</div>
);
}

View File

@@ -14,17 +14,24 @@
.popover-content {
position: absolute;
width: 350px;
animation: slide-in 0.3s ease;
right: 0;
top: calc(100% + 10px);
}
@media screen and (max-width: 600px) {
.popover-content {
width: auto;
}
}
.popover-mask {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.3);
backdrop-filter: blur(5px);
}
.list-item {
@@ -62,6 +69,7 @@
box-shadow: var(--card-shadow);
margin-bottom: 20px;
animation: slide-in ease 0.3s;
background: var(--white);
}
.list .list-item:last-child {
@@ -72,11 +80,26 @@
box-shadow: var(--card-shadow);
background-color: var(--white);
border-radius: 12px;
width: 60vw;
width: 80vw;
max-width: 900px;
min-width: 300px;
animation: slide-in ease 0.3s;
--modal-padding: 20px;
&-max {
width: 95vw;
max-width: unset;
height: 95vh;
display: flex;
flex-direction: column;
.modal-content {
max-height: unset !important;
flex-grow: 1;
}
}
.modal-header {
padding: var(--modal-padding);
display: flex;
@@ -89,11 +112,19 @@
font-size: 16px;
}
.modal-close-btn {
cursor: pointer;
.modal-header-actions {
display: flex;
&:hover {
filter: brightness(1.2);
.modal-header-action {
cursor: pointer;
&:not(:last-child) {
margin-right: 20px;
}
&:hover {
filter: brightness(1.2);
}
}
}
}
@@ -207,11 +238,11 @@
.select-with-icon {
position: relative;
max-width: fit-content;
.select-with-icon-select {
height: 100%;
border: var(--border-in-light);
padding: 10px 25px 10px 10px;
padding: 10px 35px 10px 10px;
border-radius: 10px;
appearance: none;
cursor: pointer;
@@ -227,4 +258,57 @@
transform: translateY(-50%);
pointer-events: none;
}
}
}
.modal-input {
height: 100%;
width: 100%;
border-radius: 10px;
border: var(--border-in-light);
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.03);
background-color: var(--white);
color: var(--black);
font-family: inherit;
padding: 10px;
resize: none;
outline: none;
box-sizing: border-box;
&:focus {
border: 1px solid var(--primary);
}
}
.selector {
position: fixed;
top: 0;
left: 0;
height: 100vh;
width: 100vw;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
&-content {
.list {
max-height: 90vh;
overflow-x: hidden;
overflow-y: auto;
.list-item {
cursor: pointer;
background-color: var(--white);
&:hover {
filter: brightness(0.95);
}
&:active {
filter: brightness(0.9);
}
}
}
}
}

View File

@@ -1,9 +1,16 @@
/* eslint-disable @next/next/no-img-element */
import styles from "./ui-lib.module.scss";
import LoadingIcon from "../icons/three-dots.svg";
import CloseIcon from "../icons/close.svg";
import EyeIcon from "../icons/eye.svg";
import EyeOffIcon from "../icons/eye-off.svg";
import DownIcon from "../icons/down.svg";
import ConfirmIcon from "../icons/confirm.svg";
import CancelIcon from "../icons/cancel.svg";
import MaxIcon from "../icons/max.svg";
import MinIcon from "../icons/min.svg";
import Locale from "../locales";
import { createRoot } from "react-dom/client";
import React, { HTMLProps, useEffect, useState } from "react";
@@ -19,10 +26,10 @@ export function Popover(props: {
<div className={styles.popover}>
{props.children}
{props.open && (
<div className={styles["popover-content"]}>
<div className={styles["popover-mask"]} onClick={props.onClose}></div>
{props.content}
</div>
<div className={styles["popover-mask"]} onClick={props.onClose}></div>
)}
{props.open && (
<div className={styles["popover-content"]}>{props.content}</div>
)}
</div>
);
@@ -40,9 +47,13 @@ export function ListItem(props: {
children?: JSX.Element | JSX.Element[];
icon?: JSX.Element;
className?: string;
onClick?: () => void;
}) {
return (
<div className={styles["list-item"] + ` ${props.className || ""}`}>
<div
className={styles["list-item"] + ` ${props.className || ""}`}
onClick={props.onClick}
>
<div className={styles["list-header"]}>
{props.icon && <div className={styles["list-icon"]}>{props.icon}</div>}
<div className={styles["list-item-title"]}>
@@ -59,14 +70,12 @@ export function ListItem(props: {
);
}
export function List(props: {
children:
| Array<JSX.Element | null | undefined>
| JSX.Element
| null
| undefined;
}) {
return <div className={styles.list}>{props.children}</div>;
export function List(props: { children: React.ReactNode; id?: string }) {
return (
<div className={styles.list} id={props.id}>
{props.children}
</div>
);
}
export function Loading() {
@@ -87,9 +96,12 @@ export function Loading() {
interface ModalProps {
title: string;
children?: JSX.Element | JSX.Element[];
actions?: JSX.Element[];
children?: any;
actions?: React.ReactNode[];
defaultMax?: boolean;
footer?: React.ReactNode;
onClose?: () => void;
className?: string;
}
export function Modal(props: ModalProps) {
useEffect(() => {
@@ -107,22 +119,40 @@ export function Modal(props: ModalProps) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div className={styles["modal-container"]}>
<div className={styles["modal-header"]}>
<div className={styles["modal-title"]}>{props.title}</div>
const [isMax, setMax] = useState(!!props.defaultMax);
<div className={styles["modal-close-btn"]} onClick={props.onClose}>
<CloseIcon />
return (
<div
className={`${styles["modal-container"]} ${
isMax && styles["modal-container-max"]
} ${props.className ?? ""}`}
>
<div className={`${styles["modal-header"]} new-header follow-parent-svg`}>
<div className={`${styles["modal-title"]}`}>{props.title}</div>
<div className={`${styles["modal-header-actions"]}`}>
<div
className={styles["modal-header-action"]}
onClick={() => setMax(!isMax)}
>
{isMax ? <MinIcon /> : <MaxIcon />}
</div>
<div
className={styles["modal-header-action"]}
onClick={props.onClose}
>
<CloseIcon />
</div>
</div>
</div>
<div className={styles["modal-content"]}>{props.children}</div>
<div className={styles["modal-footer"]}>
<div className={`${styles["modal-footer"]} new-footer`}>
{props.footer}
<div className={styles["modal-actions"]}>
{props.actions?.map((action, i) => (
<div key={i} className={styles["modal-action"]}>
<div key={i} className={`${styles["modal-action"]} new-btn`}>
{action}
</div>
))}
@@ -262,3 +292,197 @@ export function Select(
</div>
);
}
export function showConfirm(content: any) {
const div = document.createElement("div");
div.className = "modal-mask";
document.body.appendChild(div);
const root = createRoot(div);
const closeModal = () => {
root.unmount();
div.remove();
};
return new Promise<boolean>((resolve) => {
root.render(
<Modal
title={Locale.UI.Confirm}
actions={[
<IconButton
key="cancel"
text={Locale.UI.Cancel}
onClick={() => {
resolve(false);
closeModal();
}}
icon={<CancelIcon />}
tabIndex={0}
bordered
shadow
></IconButton>,
<IconButton
key="confirm"
text={Locale.UI.Confirm}
type="primary"
onClick={() => {
resolve(true);
closeModal();
}}
icon={<ConfirmIcon />}
tabIndex={0}
autoFocus
bordered
shadow
></IconButton>,
]}
onClose={closeModal}
>
{content}
</Modal>,
);
});
}
function PromptInput(props: {
value: string;
onChange: (value: string) => void;
rows?: number;
}) {
const [input, setInput] = useState(props.value);
const onInput = (value: string) => {
props.onChange(value);
setInput(value);
};
return (
<textarea
className={styles["modal-input"]}
autoFocus
value={input}
onInput={(e) => onInput(e.currentTarget.value)}
rows={props.rows ?? 3}
></textarea>
);
}
export function showPrompt(content: any, value = "", rows = 3) {
const div = document.createElement("div");
div.className = "modal-mask";
document.body.appendChild(div);
const root = createRoot(div);
const closeModal = () => {
root.unmount();
div.remove();
};
return new Promise<string>((resolve) => {
let userInput = value;
root.render(
<Modal
title={content}
actions={[
<IconButton
key="cancel"
text={Locale.UI.Cancel}
onClick={() => {
closeModal();
}}
icon={<CancelIcon />}
bordered
shadow
tabIndex={0}
></IconButton>,
<IconButton
key="confirm"
text={Locale.UI.Confirm}
type="primary"
onClick={() => {
resolve(userInput);
closeModal();
}}
icon={<ConfirmIcon />}
bordered
shadow
tabIndex={0}
></IconButton>,
]}
onClose={closeModal}
>
<PromptInput
onChange={(val) => (userInput = val)}
value={value}
rows={rows}
></PromptInput>
</Modal>,
);
});
}
export function showImageModal(img: string) {
showModal({
title: Locale.Export.Image.Modal,
children: (
<div>
<img
src={img}
alt="preview"
style={{
maxWidth: "100%",
}}
></img>
</div>
),
});
}
export function Selector<T>(props: {
items: Array<{
title: string;
subTitle?: string;
value: T;
}>;
defaultSelectedValue?: T;
onSelection?: (selection: T[]) => void;
onClose?: () => void;
multiple?: boolean;
}) {
return (
<div className={styles["selector"]} onClick={() => props.onClose?.()}>
<div className={styles["selector-content"]}>
<List>
{props.items.map((item, i) => {
const selected = props.defaultSelectedValue === item.value;
return (
<ListItem
className={styles["selector-item"]}
key={i}
title={item.title}
subTitle={item.subTitle}
onClick={() => {
props.onSelection?.([item.value]);
props.onClose?.();
}}
>
{selected ? (
<div
style={{
height: 10,
width: 10,
backgroundColor: "var(--primary)",
borderRadius: 10,
}}
></div>
) : (
<></>
)}
</ListItem>
);
})}
</List>
</div>
</div>
);
}

View File

@@ -1,3 +1,5 @@
import tauriConfig from "../../src-tauri/tauri.conf.json";
export const getBuildConfig = () => {
if (typeof process === "undefined") {
throw Error(
@@ -5,23 +7,37 @@ export const getBuildConfig = () => {
);
}
const COMMIT_ID: string = (() => {
const buildMode = process.env.BUILD_MODE ?? "standalone";
const isApp = !!process.env.BUILD_APP;
const version = "v" + tauriConfig.package.version;
const commitInfo = (() => {
try {
const childProcess = require("child_process");
return childProcess
const commitDate: string = childProcess
.execSync('git log -1 --format="%at000" --date=unix')
.toString()
.trim();
const commitHash: string = childProcess
.execSync('git log --pretty=format:"%H" -n 1')
.toString()
.trim();
return { commitDate, commitHash };
} catch (e) {
console.error("[Build Config] No git or not from git repo.");
return "unknown";
return {
commitDate: "unknown",
commitHash: "unknown",
};
}
})();
return {
commitId: COMMIT_ID,
buildMode: process.env.BUILD_MODE ?? "standalone",
isApp: !!process.env.BUILD_APP,
version,
...commitInfo,
buildMode,
isApp,
};
};

View File

@@ -3,7 +3,12 @@ import { BuildConfig, getBuildConfig } from "./build";
export function getClientConfig() {
if (typeof document !== "undefined") {
// client side
return JSON.parse(queryMeta("config")) as BuildConfig;
try {
const config = JSON.parse(queryMeta("config")) as BuildConfig;
return config;
} catch (e) {
return null;
}
}
if (typeof process !== "undefined") {

View File

@@ -1,17 +1,39 @@
import md5 from "spark-md5";
import { DEFAULT_MODELS } from "../constant";
declare global {
namespace NodeJS {
interface ProcessEnv {
PROXY_URL?: string; // docker only
OPENAI_API_KEY?: string;
CODE?: string;
BASE_URL?: string;
PROXY_URL?: string;
OPENAI_ORG_ID?: string; // openai only
VERCEL?: string;
HIDE_USER_API_KEY?: string; // disable user's api key input
DISABLE_GPT4?: string; // allow user to use gpt-4 or not
BUILD_MODE?: "standalone" | "export";
BUILD_APP?: string; // is building desktop app
HIDE_USER_API_KEY?: string; // disable user's api key input
DISABLE_GPT4?: string; // allow user to use gpt-4 or not
ENABLE_BALANCE_QUERY?: string; // allow user to query balance or not
DISABLE_FAST_LINK?: string; // disallow parse settings from url or not
CUSTOM_MODELS?: string; // to control custom models
DEFAULT_MODEL?: string; // to cnntrol default model in every new chat window
// azure only
AZURE_URL?: string; // https://{azure-url}/openai/deployments/{deploy-name}
AZURE_API_KEY?: string;
AZURE_API_VERSION?: string;
// google only
GOOGLE_API_KEY?: string;
GOOGLE_URL?: string;
// google tag manager
GTM_ID?: string;
}
}
}
@@ -36,15 +58,68 @@ export const getServerSideConfig = () => {
);
}
const disableGPT4 = !!process.env.DISABLE_GPT4;
let customModels = process.env.CUSTOM_MODELS ?? "";
let defaultModel = process.env.DEFAULT_MODEL ?? "";
if (disableGPT4) {
if (customModels) customModels += ",";
customModels += DEFAULT_MODELS.filter((m) => m.name.startsWith("gpt-4"))
.map((m) => "-" + m.name)
.join(",");
if (defaultModel.startsWith("gpt-4")) defaultModel = "";
}
const isAzure = !!process.env.AZURE_URL;
const isGoogle = !!process.env.GOOGLE_API_KEY;
const isAnthropic = !!process.env.ANTHROPIC_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);
const apiKey = apiKeys[randomIndex];
console.log(
`[Server Config] using ${randomIndex + 1} of ${apiKeys.length} api key`,
);
const whiteWebDevEndpoints = (process.env.WHITE_WEBDEV_ENDPOINTS ?? "").split(
",",
);
return {
apiKey: process.env.OPENAI_API_KEY,
baseUrl: process.env.BASE_URL,
apiKey,
openaiOrgId: process.env.OPENAI_ORG_ID,
isAzure,
azureUrl: process.env.AZURE_URL,
azureApiKey: process.env.AZURE_API_KEY,
azureApiVersion: process.env.AZURE_API_VERSION,
isGoogle,
googleApiKey: process.env.GOOGLE_API_KEY,
googleUrl: process.env.GOOGLE_URL,
isAnthropic,
anthropicApiKey: process.env.ANTHROPIC_API_KEY,
anthropicApiVersion: process.env.ANTHROPIC_API_VERSION,
anthropicUrl: process.env.ANTHROPIC_URL,
gtmId: process.env.GTM_ID,
needCode: ACCESS_CODES.size > 0,
code: process.env.CODE,
codes: ACCESS_CODES,
needCode: ACCESS_CODES.size > 0,
baseUrl: process.env.BASE_URL,
proxyUrl: process.env.PROXY_URL,
isVercel: !!process.env.VERCEL,
hideUserApiKey: !!process.env.HIDE_USER_API_KEY,
enableGPT4: !process.env.DISABLE_GPT4,
disableGPT4,
hideBalanceQuery: !process.env.ENABLE_BALANCE_QUERY,
disableFastLink: !!process.env.DISABLE_FAST_LINK,
customModels,
defaultModel,
whiteWebDevEndpoints,
};
};

View File

@@ -3,10 +3,16 @@ export const REPO = "ChatGPT-Next-Web";
export const REPO_URL = `https://github.com/${OWNER}/${REPO}`;
export const ISSUE_URL = `https://github.com/${OWNER}/${REPO}/issues`;
export const UPDATE_URL = `${REPO_URL}#keep-updated`;
export const RELEASE_URL = `${REPO_URL}/releases`;
export const FETCH_COMMIT_URL = `https://api.github.com/repos/${OWNER}/${REPO}/commits?per_page=1`;
export const FETCH_TAG_URL = `https://api.github.com/repos/${OWNER}/${REPO}/tags?per_page=1`;
export const RUNTIME_CONFIG_DOM = "danger-runtime-config";
export const DEFAULT_API_HOST = "https://chatgpt1.nextweb.fun/api/proxy";
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";
export const GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/";
export enum Path {
Home = "/",
@@ -17,8 +23,15 @@ export enum Path {
Auth = "/auth",
}
export enum ApiPath {
Cors = "",
OpenAI = "/api/openai",
Anthropic = "/api/anthropic",
}
export enum SlotID {
AppBody = "app-body",
CustomModel = "custom-model",
}
export enum FileName {
@@ -33,22 +46,184 @@ export enum StoreKey {
Mask = "mask-store",
Prompt = "prompt-store",
Update = "chat-update",
Sync = "sync",
}
export const MAX_SIDEBAR_WIDTH = 500;
export const MIN_SIDEBAR_WIDTH = 230;
export const NARROW_SIDEBAR_WIDTH = 100;
export const ACCESS_CODE_PREFIX = "ak-";
export const DEFAULT_SIDEBAR_WIDTH = 340;
export const MAX_SIDEBAR_WIDTH = 440;
export const MIN_SIDEBAR_WIDTH = 230;
export const WINDOW_WIDTH_SM = 480;
export const WINDOW_WIDTH_MD = 768;
export const WINDOW_WIDTH_LG = 1120;
export const WINDOW_WIDTH_XL = 1440;
export const WINDOW_WIDTH_2XL = 1980;
export const ACCESS_CODE_PREFIX = "nk-";
export const LAST_INPUT_KEY = "last-input";
export const UNFINISHED_INPUT = (id: string) => "unfinished-input-" + id;
export const STORAGE_KEY = "chatgpt-next-web";
export const REQUEST_TIMEOUT_MS = 60000;
export const EXPORT_MESSAGE_CLASS_NAME = "export-markdown";
export enum ServiceProvider {
OpenAI = "OpenAI",
Azure = "Azure",
Google = "Google",
Anthropic = "Anthropic",
}
export enum ModelProvider {
GPT = "GPT",
GeminiPro = "GeminiPro",
Claude = "Claude",
}
export const Anthropic = {
ChatPath: "v1/messages",
ChatPath1: "v1/complete",
ExampleEndpoint: "https://api.anthropic.com",
Vision: "2023-06-01",
};
export const OpenaiPath = {
ChatPath: "v1/chat/completions",
UsagePath: "dashboard/billing/usage",
SubsPath: "dashboard/billing/subscription",
ListModelPath: "v1/models",
};
export const Azure = {
ExampleEndpoint: "https://{resource-url}/openai/deployments/{deploy-id}",
};
export const Google = {
ExampleEndpoint: "https://generativelanguage.googleapis.com/",
ChatPath: (modelName: string) => `v1beta/models/${modelName}:generateContent`,
VisionChatPath: (modelName: string) => `v1beta/models/${modelName}:generateContent`,
};
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}}.
// Knowledge cutoff: {{cutoff}}
// Current model: {{model}}
// Current time: {{time}}
// Latex inline: $x^2$
// Latex block: $$e=mc^2$$
// `;
export const DEFAULT_SYSTEM_TEMPLATE = `
You are ChatGPT, a large language model trained by {{ServiceProvider}}.
Knowledge cutoff: {{cutoff}}
Current model: {{model}}
Current time: {{time}}
Latex inline: \\(x^2\\)
Latex block: $$e=mc^2$$
`;
export const SUMMARIZE_MODEL = "gpt-3.5-turbo";
export const GEMINI_SUMMARIZE_MODEL = "gemini-pro";
export const KnowledgeCutOffDate: Record<string, string> = {
default: "2021-09",
"gpt-4-turbo": "2023-12",
"gpt-4-turbo-2024-04-09": "2023-12",
"gpt-4-turbo-preview": "2023-12",
"gpt-4-1106-preview": "2023-04",
"gpt-4-0125-preview": "2023-12",
"gpt-4-vision-preview": "2023-04",
// 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",
};
const openaiModels = [
"gpt-3.5-turbo",
"gpt-3.5-turbo-0301",
"gpt-3.5-turbo-0613",
"gpt-3.5-turbo-1106",
"gpt-3.5-turbo-0125",
"gpt-3.5-turbo-16k",
"gpt-3.5-turbo-16k-0613",
"gpt-4",
"gpt-4-0314",
"gpt-4-0613",
"gpt-4-1106-preview",
"gpt-4-0125-preview",
"gpt-4-32k",
"gpt-4-32k-0314",
"gpt-4-32k-0613",
"gpt-4-turbo",
"gpt-4-turbo-preview",
"gpt-4-vision-preview",
"gpt-4-turbo-2024-04-09",
];
const googleModels = [
"gemini-1.0-pro",
"gemini-1.5-pro-latest",
"gemini-pro-vision",
];
const anthropicModels = [
"claude-instant-1.2",
"claude-2.0",
"claude-2.1",
"claude-3-sonnet-20240229",
"claude-3-opus-20240229",
"claude-3-haiku-20240307",
];
export const DEFAULT_MODELS = [
...openaiModels.map((name) => ({
name,
available: true,
provider: {
id: "openai",
providerName: "OpenAI",
providerType: "openai",
},
})),
...googleModels.map((name) => ({
name,
available: true,
provider: {
id: "google",
providerName: "Google",
providerType: "google",
},
})),
...anthropicModels.map((name) => ({
name,
available: true,
provider: {
id: "anthropic",
providerName: "Anthropic",
providerType: "anthropic",
},
})),
] as const;
export const CHAT_PAGE_SIZE = 15;
export const MAX_RENDER_MSG_COUNT = 45;
// some famous webdav endpoints
export const internalWhiteWebDavEndpoints = [
"https://dav.jianguoyun.com/dav/",
"https://dav.dropdav.com/",
"https://dav.box.com/dav",
"https://nanao.teracloud.jp/dav/",
"https://webdav.4shared.com/",
"https://dav.idrivesync.com",
"https://webdav.yandex.com",
"https://app.koofr.net/dav/Koofr",
];
export const SIDEBAR_ID = "sidebar";

View File

@@ -0,0 +1,301 @@
import React, { useState, useRef, useEffect, useMemo } from "react";
import {
useChatStore,
BOT_HELLO,
createMessage,
useAccessStore,
useAppConfig,
ModelType,
} from "@/app/store";
import Locale from "@/app/locales";
import { Selector, showConfirm, showToast } from "@/app/components/ui-lib";
import {
CHAT_PAGE_SIZE,
REQUEST_TIMEOUT_MS,
UNFINISHED_INPUT,
} from "@/app/constant";
import { useCommand } from "@/app/command";
import { prettyObject } from "@/app/utils/format";
import { ExportMessageModal } from "@/app/components/exporter";
import PromptToast from "./components/PromptToast";
import { EditMessageModal } from "./components/EditMessageModal";
import ChatHeader from "./components/ChatHeader";
import ChatInputPanel, {
ChatInputPanelInstance,
} from "./components/ChatInputPanel";
import ChatMessagePanel, { RenderMessage } from "./components/ChatMessagePanel";
import { useAllModels } from "@/app/utils/hooks";
import useRows from "@/app/hooks/useRows";
import SessionConfigModel from "./components/SessionConfigModal";
import useScrollToBottom from "@/app/hooks/useScrollToBottom";
function _Chat() {
const chatStore = useChatStore();
const session = chatStore.currentSession();
const config = useAppConfig();
const { isMobileScreen } = config;
const [showExport, setShowExport] = useState(false);
const inputRef = useRef<HTMLTextAreaElement>(null);
const [userInput, setUserInput] = useState("");
const [isLoading, setIsLoading] = useState(false);
const scrollRef = useRef<HTMLDivElement>(null);
const chatInputPanelRef = useRef<ChatInputPanelInstance | null>(null);
const [hitBottom, setHitBottom] = useState(true);
const [attachImages, setAttachImages] = useState<string[]>([]);
// auto grow input
const { measure, inputRows } = useRows({
inputRef,
});
const { setAutoScroll, scrollDomToBottom } = useScrollToBottom(scrollRef);
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(measure, [userInput]);
useEffect(() => {
chatStore.updateCurrentSession((session) => {
const stopTiming = Date.now() - REQUEST_TIMEOUT_MS;
session.messages.forEach((m) => {
// check if should stop all stale messages
if (m.isError || new Date(m.date).getTime() < stopTiming) {
if (m.streaming) {
m.streaming = false;
}
if (m.content.length === 0) {
m.isError = true;
m.content = prettyObject({
error: true,
message: "empty response",
});
}
}
});
// auto sync mask config from global config
if (session.mask.syncGlobalConfig) {
console.log("[Mask] syncing from global, name = ", session.mask.name);
session.mask.modelConfig = { ...config.modelConfig };
}
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const context: RenderMessage[] = useMemo(() => {
return session.mask.hideContext ? [] : session.mask.context.slice();
}, [session.mask.context, session.mask.hideContext]);
const accessStore = useAccessStore();
if (
context.length === 0 &&
session.messages.at(0)?.content !== BOT_HELLO.content
) {
const copiedHello = Object.assign({}, BOT_HELLO);
if (!accessStore.isAuthorized()) {
copiedHello.content = Locale.Error.Unauthorized;
}
context.push(copiedHello);
}
// preview messages
const renderMessages = useMemo(() => {
return context
.concat(session.messages as RenderMessage[])
.concat(
isLoading
? [
{
...createMessage({
role: "assistant",
content: "……",
}),
preview: true,
},
]
: [],
)
.concat(
userInput.length > 0 && config.sendPreviewBubble
? [
{
...createMessage(
{
role: "user",
content: userInput,
},
{
customId: "typing",
},
),
preview: true,
},
]
: [],
);
}, [
config.sendPreviewBubble,
context,
isLoading,
session.messages,
userInput,
]);
const [msgRenderIndex, _setMsgRenderIndex] = useState(
Math.max(0, renderMessages.length - CHAT_PAGE_SIZE),
);
const [showPromptModal, setShowPromptModal] = useState(false);
useCommand({
fill: setUserInput,
submit: (text) => {
chatInputPanelRef.current?.doSubmit(text);
},
code: (text) => {
if (accessStore.disableFastLink) return;
console.log("[Command] got code from url: ", text);
showConfirm(Locale.URLCommand.Code + `code = ${text}`).then((res) => {
if (res) {
accessStore.update((access) => (access.accessCode = text));
}
});
},
settings: (text) => {
if (accessStore.disableFastLink) return;
try {
const payload = JSON.parse(text) as {
key?: string;
url?: string;
};
console.log("[Command] got settings from url: ", payload);
if (payload.key || payload.url) {
showConfirm(
Locale.URLCommand.Settings +
`\n${JSON.stringify(payload, null, 4)}`,
).then((res) => {
if (!res) return;
if (payload.key) {
accessStore.update(
(access) => (access.openaiApiKey = payload.key!),
);
}
if (payload.url) {
accessStore.update((access) => (access.openaiUrl = payload.url!));
}
});
}
} catch {
console.error("[Command] failed to get settings from url: ", text);
}
},
});
// edit / insert message modal
const [isEditingMessage, setIsEditingMessage] = useState(false);
// remember unfinished input
useEffect(() => {
// try to load from local storage
const key = UNFINISHED_INPUT(session.id);
const mayBeUnfinishedInput = localStorage.getItem(key);
if (mayBeUnfinishedInput && userInput.length === 0) {
setUserInput(mayBeUnfinishedInput);
localStorage.removeItem(key);
}
const dom = inputRef.current;
return () => {
localStorage.setItem(key, dom?.value ?? "");
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const chatinputPanelProps = {
inputRef,
isMobileScreen,
renderMessages,
attachImages,
userInput,
hitBottom,
inputRows,
setAttachImages,
setUserInput,
setIsLoading,
showChatSetting: setShowPromptModal,
_setMsgRenderIndex,
scrollDomToBottom,
setAutoScroll,
};
const chatMessagePanelProps = {
scrollRef,
inputRef,
isMobileScreen,
msgRenderIndex,
userInput,
context,
renderMessages,
setAutoScroll,
setMsgRenderIndex: chatInputPanelRef.current?.setMsgRenderIndex,
setHitBottom,
setUserInput,
setIsLoading,
setShowPromptModal,
scrollDomToBottom,
};
return (
<div
className={`
relative flex flex-col overflow-hidden bg-chat-panel
max-md:absolute max-md:h-[100vh] max-md:w-[100%]
md:h-[100%] md:mr-2.5 md:rounded-md
`}
key={session.id}
>
<ChatHeader
setIsEditingMessage={setIsEditingMessage}
setShowExport={setShowExport}
isMobileScreen={isMobileScreen}
/>
<ChatMessagePanel {...chatMessagePanelProps} />
<ChatInputPanel ref={chatInputPanelRef} {...chatinputPanelProps} />
{showExport && (
<ExportMessageModal onClose={() => setShowExport(false)} />
)}
{isEditingMessage && (
<EditMessageModal
onClose={() => {
setIsEditingMessage(false);
}}
/>
)}
<PromptToast showToast={!hitBottom} setShowModal={setShowPromptModal} />
{showPromptModal && (
<SessionConfigModel onClose={() => setShowPromptModal(false)} />
)}
</div>
);
}
export default function Chat() {
const chatStore = useChatStore();
const sessionIndex = chatStore.currentSessionIndex;
return <_Chat key={sessionIndex}></_Chat>;
}

View File

@@ -0,0 +1,276 @@
import { ModelType, Theme, useAppConfig } from "@/app/store/config";
import { useChatStore } from "@/app/store/chat";
import { ChatControllerPool } from "@/app/client/controller";
import { useAllModels } from "@/app/utils/hooks";
import { useEffect, useMemo, useState } from "react";
import { isVisionModel } from "@/app/utils";
import { showToast } from "@/app/components/ui-lib";
import Locale from "@/app/locales";
import { Path } from "@/app/constant";
import BottomIcon from "@/app/icons/bottom.svg";
import StopIcon from "@/app/icons/pause.svg";
import LoadingButtonIcon from "@/app/icons/loading.svg";
import PromptIcon from "@/app/icons/comandIcon.svg";
import MaskIcon from "@/app/icons/maskIcon.svg";
import BreakIcon from "@/app/icons/eraserIcon.svg";
import SettingsIcon from "@/app/icons/configIcon.svg";
import ImageIcon from "@/app/icons/uploadImgIcon.svg";
import AddCircleIcon from "@/app/icons/addCircle.svg";
import Popover from "@/app/components/Popover";
import ModelSelect from "./ModelSelect";
import { useRouter } from "next/navigation";
export interface Action {
onClick?: () => void;
text: string;
isShow: boolean;
render?: (key: string) => JSX.Element;
icon?: JSX.Element;
placement: "left" | "right";
className?: string;
}
export function ChatActions(props: {
uploadImage: () => void;
setAttachImages: (images: string[]) => void;
setUploading: (uploading: boolean) => void;
showChatSetting: () => void;
scrollToBottom: () => void;
showPromptHints: () => void;
hitBottom: boolean;
uploading: boolean;
isMobileScreen: boolean;
className?: string;
}) {
const config = useAppConfig();
const router = useRouter();
const chatStore = useChatStore();
// switch themes
const theme = config.theme;
function nextTheme() {
const themes = [Theme.Auto, Theme.Light, Theme.Dark];
const themeIndex = themes.indexOf(theme);
const nextIndex = (themeIndex + 1) % themes.length;
const nextTheme = themes[nextIndex];
config.update((config) => (config.theme = nextTheme));
}
// stop all responses
const couldStop = ChatControllerPool.hasPending();
const stopAll = () => ChatControllerPool.stopAll();
// switch model
const currentModel = chatStore.currentSession().mask.modelConfig.model;
const allModels = useAllModels();
const models = useMemo(
() => allModels.filter((m) => m.available),
[allModels],
);
const [showUploadImage, setShowUploadImage] = useState(false);
useEffect(() => {
const show = isVisionModel(currentModel);
setShowUploadImage(show);
if (!show) {
props.setAttachImages([]);
props.setUploading(false);
}
// if current model is not available
// switch to first available model
const isUnavaliableModel = !models.some((m) => m.name === currentModel);
if (isUnavaliableModel && models.length > 0) {
const nextModel = models[0].name as ModelType;
chatStore.updateCurrentSession(
(session) => (session.mask.modelConfig.model = nextModel),
);
showToast(nextModel);
}
}, [chatStore, currentModel, models]);
const actions: Action[] = [
{
onClick: stopAll,
text: Locale.Chat.InputActions.Stop,
isShow: couldStop,
icon: <StopIcon />,
placement: "left",
},
{
text: currentModel,
isShow: !props.isMobileScreen,
render: (key: string) => <ModelSelect key={key} />,
placement: "left",
},
{
onClick: props.scrollToBottom,
text: Locale.Chat.InputActions.ToBottom,
isShow: !props.hitBottom,
icon: <BottomIcon />,
placement: "left",
},
{
onClick: props.uploadImage,
text: Locale.Chat.InputActions.UploadImage,
isShow: showUploadImage,
icon: props.uploading ? <LoadingButtonIcon /> : <ImageIcon />,
placement: "left",
},
// {
// onClick: nextTheme,
// text: Locale.Chat.InputActions.Theme[theme],
// isShow: true,
// icon: (
// <>
// {theme === Theme.Auto ? (
// <AutoIcon />
// ) : theme === Theme.Light ? (
// <LightIcon />
// ) : theme === Theme.Dark ? (
// <DarkIcon />
// ) : null}
// </>
// ),
// placement: "left",
// },
{
onClick: props.showPromptHints,
text: Locale.Chat.InputActions.Prompt,
isShow: true,
icon: <PromptIcon />,
placement: "left",
},
{
onClick: () => {
router.push(Path.Masks);
},
text: Locale.Chat.InputActions.Masks,
isShow: true,
icon: <MaskIcon />,
placement: "left",
},
{
onClick: () => {
chatStore.updateCurrentSession((session) => {
if (session.clearContextIndex === session.messages.length) {
session.clearContextIndex = undefined;
} else {
session.clearContextIndex = session.messages.length;
session.memoryPrompt = ""; // will clear memory
}
});
},
text: Locale.Chat.InputActions.Clear,
isShow: true,
icon: <BreakIcon />,
placement: "right",
},
{
onClick: props.showChatSetting,
text: Locale.Chat.InputActions.Settings,
isShow: true,
icon: <SettingsIcon />,
placement: "right",
},
];
if (props.isMobileScreen) {
const content = (
<div className="w-[100%]">
{actions
.filter((v) => v.isShow && v.icon)
.map((act) => {
return (
<div
key={act.text}
className={`flex items-center gap-3 p-3 rounded-action-btn leading-6 cursor-pointer follow-parent-svg default-icon-color`}
onClick={act.onClick}
>
{act.icon}
<div className="flex-1 font-common text-actions-popover-menu-item">
{act.text}
</div>
</div>
);
})}
</div>
);
return (
<Popover
content={content}
trigger="click"
placement="rt"
noArrow
popoverClassName="border border-chat-actions-popover-mobile rounded-md shadow-chat-actions-popover-mobile w-actions-popover bg-chat-actions-popover-panel-mobile "
className="cursor-pointer follow-parent-svg default-icon-color"
>
<AddCircleIcon />
</Popover>
);
}
const popoverClassName = `bg-chat-actions-btn-popover px-3 py-2.5 text-text-chat-actions-btn-popover text-sm-title rounded-md`;
return (
<div className={`flex gap-2 item-center ${props.className}`}>
{actions
.filter((v) => v.placement === "left" && v.isShow)
.map((act, ind) => {
if (act.render) {
return (
<div className={`${act.className ?? ""}`} key={act.text}>
{act.render(act.text)}
</div>
);
}
return (
<Popover
key={act.text}
content={act.text}
popoverClassName={`${popoverClassName}`}
placement={ind ? "t" : "lt"}
className={`${act.className ?? ""}`}
>
<div
className={`
cursor-pointer h-[32px] w-[32px] flex items-center justify-center transition duration-300 ease-in-out
hover:bg-chat-actions-btn-hovered hover:rounded-action-btn
follow-parent-svg default-icon-color
`}
onClick={act.onClick}
>
{act.icon}
</div>
</Popover>
);
})}
<div className="flex-1"></div>
{actions
.filter((v) => v.placement === "right" && v.isShow)
.map((act, ind, arr) => {
return (
<Popover
key={act.text}
content={act.text}
popoverClassName={`${popoverClassName}`}
placement={ind === arr.length - 1 ? "rt" : "t"}
>
<div
className={`
cursor-pointer h-[32px] w-[32px] flex items-center transition duration-300 ease-in-out justify-center
hover:bg-chat-actions-btn-hovered hover:rounded-action-btn
follow-parent-svg default-icon-color
`}
onClick={act.onClick}
>
{act.icon}
</div>
</Popover>
);
})}
</div>
);
}

View File

@@ -0,0 +1,91 @@
import { useRouter } from "next/navigation";
import Locale from "@/app/locales";
import { Path } from "@/app/constant";
import { DEFAULT_TOPIC, useChatStore } from "@/app/store/chat";
import LogIcon from "@/app/icons/logIcon.svg";
import GobackIcon from "@/app/icons/goback.svg";
import ShareIcon from "@/app/icons/shareIcon.svg";
import ModelSelect from "./ModelSelect";
export interface ChatHeaderProps {
isMobileScreen: boolean;
setIsEditingMessage: (v: boolean) => void;
setShowExport: (v: boolean) => void;
}
export default function ChatHeader(props: ChatHeaderProps) {
const { isMobileScreen, setIsEditingMessage, setShowExport } = props;
// const navigate = useNavigate();
const router = useRouter();
const chatStore = useChatStore();
const session = chatStore.currentSession();
return (
<div
className={`
absolute w-[100%] backdrop-blur-[30px] z-20 flex flex-0 justify-between items-center px-6 py-4 gap-chat-header-gap
sm:border-b sm:border-chat-header-bottom
max-md:h-menu-title-mobile
`}
data-tauri-drag-region
>
<div
className={`absolute z-[-1] top-0 left-0 w-[100%] h-[100%] opacity-85 backdrop-blur-[20px] sm:bg-chat-panel-header-mask bg-chat-panel-header-mobile flex flex-0 justify-between items-center gap-chat-header-gap`}
>
{" "}
</div>
{isMobileScreen ? (
<div
className="cursor-pointer follow-parent-svg default-icon-color"
onClick={() => router.push(Path.Home)}
>
<GobackIcon />
</div>
) : (
<LogIcon />
)}
<div
className={`
flex-1
max-md:flex max-md:flex-col max-md:items-center max-md:justify-center max-md:gap-0.5 max-md:text
md:mr-4
`}
>
<div
className={`
line-clamp-1 cursor-pointer text-text-chat-header-title text-chat-header-title font-common
max-md:text-sm-title max-md:h-chat-header-title-mobile max-md:leading-5
`}
onClickCapture={() => setIsEditingMessage(true)}
>
{!session.topic ? DEFAULT_TOPIC : session.topic}
</div>
<div
className={`
text-text-chat-header-subtitle text-sm
max-md:text-sm-mobile-tab max-md:leading-4
`}
>
{isMobileScreen ? (
<ModelSelect />
) : (
Locale.Chat.SubTitle(session.messages.length)
)}
</div>
</div>
<div
className=" cursor-pointer hover:bg-hovered-btn p-1.5 rounded-action-btn follow-parent-svg default-icon-color"
onClick={() => {
setShowExport(true);
}}
>
<ShareIcon />
</div>
</div>
);
}

View File

@@ -0,0 +1,323 @@
import { forwardRef, useImperativeHandle, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useDebouncedCallback } from "use-debounce";
import useUploadImage from "@/app/hooks/useUploadImage";
import Locale from "@/app/locales";
import useSubmitHandler from "@/app/hooks/useSubmitHandler";
import { CHAT_PAGE_SIZE, LAST_INPUT_KEY, Path } from "@/app/constant";
import { ChatCommandPrefix, useChatCommand } from "@/app/command";
import { useChatStore } from "@/app/store/chat";
import { usePromptStore } from "@/app/store/prompt";
import { useAppConfig } from "@/app/store/config";
import { useRouter } from "next/navigation";
import usePaste from "@/app/hooks/usePaste";
import { ChatActions } from "./ChatActions";
import PromptHints, { RenderPompt } from "./PromptHint";
// import CEIcon from "@/app/icons/command&enterIcon.svg";
// import EnterIcon from "@/app/icons/enterIcon.svg";
import SendIcon from "@/app/icons/sendIcon.svg";
import Btn from "@/app/components/Btn";
import Thumbnail from "@/app/components/ThumbnailImg";
export interface ChatInputPanelProps {
inputRef: React.RefObject<HTMLTextAreaElement>;
isMobileScreen: boolean;
renderMessages: any[];
attachImages: string[];
userInput: string;
hitBottom: boolean;
inputRows: number;
setAttachImages: (imgs: string[]) => void;
setUserInput: (v: string) => void;
setIsLoading: (value: boolean) => void;
showChatSetting: (value: boolean) => void;
_setMsgRenderIndex: (value: number) => void;
setAutoScroll: (value: boolean) => void;
scrollDomToBottom: () => void;
}
export interface ChatInputPanelInstance {
setUploading: (v: boolean) => void;
doSubmit: (userInput: string) => void;
setMsgRenderIndex: (v: number) => void;
}
// only search prompts when user input is short
const SEARCH_TEXT_LIMIT = 30;
export default forwardRef<ChatInputPanelInstance, ChatInputPanelProps>(
function ChatInputPanel(props, ref) {
const {
attachImages,
inputRef,
setAttachImages,
userInput,
isMobileScreen,
setUserInput,
setIsLoading,
showChatSetting,
renderMessages,
_setMsgRenderIndex,
hitBottom,
inputRows,
setAutoScroll,
scrollDomToBottom,
} = props;
const [uploading, setUploading] = useState(false);
const [promptHints, setPromptHints] = useState<RenderPompt[]>([]);
const chatStore = useChatStore();
const router = useRouter();
const config = useAppConfig();
const { uploadImage } = useUploadImage(attachImages, {
emitImages: setAttachImages,
setUploading,
});
const { submitKey, shouldSubmit } = useSubmitHandler();
const autoFocus = !isMobileScreen; // wont auto focus on mobile screen
// chat commands shortcuts
const chatCommands = useChatCommand({
new: () => chatStore.newSession(),
newm: () => router.push(Path.NewChat),
prev: () => chatStore.nextSession(-1),
next: () => chatStore.nextSession(1),
clear: () =>
chatStore.updateCurrentSession(
(session) => (session.clearContextIndex = session.messages.length),
),
del: () => chatStore.deleteSession(chatStore.currentSessionIndex),
});
// prompt hints
const promptStore = usePromptStore();
const onSearch = useDebouncedCallback(
(text: string) => {
const matchedPrompts = promptStore.search(text);
setPromptHints(matchedPrompts);
},
100,
{ leading: true, trailing: true },
);
// check if should send message
const onInputKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
// if ArrowUp and no userInput, fill with last input
if (
e.key === "ArrowUp" &&
userInput.length <= 0 &&
!(e.metaKey || e.altKey || e.ctrlKey)
) {
setUserInput(localStorage.getItem(LAST_INPUT_KEY) ?? "");
e.preventDefault();
return;
}
if (shouldSubmit(e) && promptHints.length === 0) {
doSubmit(userInput);
e.preventDefault();
}
};
const onPromptSelect = (prompt: RenderPompt) => {
setTimeout(() => {
setPromptHints([]);
const matchedChatCommand = chatCommands.match(prompt.content);
if (matchedChatCommand.matched) {
// if user is selecting a chat command, just trigger it
matchedChatCommand.invoke();
setUserInput("");
} else {
// or fill the prompt
setUserInput(prompt.content);
}
inputRef.current?.focus();
}, 30);
};
const doSubmit = (userInput: string) => {
if (userInput.trim() === "") return;
const matchCommand = chatCommands.match(userInput);
if (matchCommand.matched) {
setUserInput("");
setPromptHints([]);
matchCommand.invoke();
return;
}
setIsLoading(true);
chatStore
.onUserInput(userInput, attachImages)
.then(() => setIsLoading(false));
setAttachImages([]);
localStorage.setItem(LAST_INPUT_KEY, userInput);
setUserInput("");
setPromptHints([]);
if (!isMobileScreen) inputRef.current?.focus();
setAutoScroll(true);
};
useImperativeHandle(ref, () => ({
setUploading,
doSubmit,
setMsgRenderIndex,
}));
function scrollToBottom() {
setMsgRenderIndex(renderMessages.length - CHAT_PAGE_SIZE);
scrollDomToBottom();
}
const onInput = (text: string) => {
setUserInput(text);
const n = text.trim().length;
// clear search results
if (n === 0) {
setPromptHints([]);
} else if (text.startsWith(ChatCommandPrefix)) {
setPromptHints(chatCommands.search(text));
} else if (!config.disablePromptHint && n < SEARCH_TEXT_LIMIT) {
// check if need to trigger auto completion
if (text.startsWith("/")) {
let searchText = text.slice(1);
onSearch(searchText);
}
}
};
function setMsgRenderIndex(newIndex: number) {
newIndex = Math.min(renderMessages.length - CHAT_PAGE_SIZE, newIndex);
newIndex = Math.max(0, newIndex);
_setMsgRenderIndex(newIndex);
}
const { handlePaste } = usePaste(attachImages, {
emitImages: setAttachImages,
setUploading,
});
return (
<div
className={`
relative w-[100%] box-border
max-md:rounded-tl-md max-md:rounded-tr-md
md:border-t md:border-chat-input-top
`}
>
<PromptHints
prompts={promptHints}
onPromptSelect={onPromptSelect}
className=" border-chat-input-top"
/>
<div
className={`
flex
max-md:flex-row-reverse max-md:items-center max-md:gap-2 max-md:p-3
md:flex-col md:px-5 md:pb-5
`}
>
<ChatActions
uploadImage={uploadImage}
setAttachImages={setAttachImages}
setUploading={setUploading}
showChatSetting={() => showChatSetting(true)}
scrollToBottom={scrollToBottom}
hitBottom={hitBottom}
uploading={uploading}
showPromptHints={() => {
// Click again to close
if (promptHints.length > 0) {
setPromptHints([]);
return;
}
inputRef.current?.focus();
setUserInput("/");
onSearch("");
}}
className={`
md:py-2.5
`}
isMobileScreen={isMobileScreen}
/>
<label
className={`
cursor-text flex flex-col bg-chat-panel-input-hood border border-chat-input-hood
focus-within:border-chat-input-hood-focus sm:focus-within:shadow-chat-input-hood-focus-shadow
rounded-chat-input p-3 gap-3 max-md:flex-1
md:rounded-md md:p-4 md:gap-4
`}
htmlFor="chat-input"
>
{attachImages.length != 0 && (
<div className={`flex gap-2`}>
{attachImages.map((image, index) => {
return (
<Thumbnail
key={index}
deleteImage={() => {
setAttachImages(
attachImages.filter((_, i) => i !== index),
);
}}
image={image}
/>
);
})}
</div>
)}
<textarea
id="chat-input"
ref={inputRef}
className={`
leading-[19px] flex-1 focus:outline-none focus:shadow-none focus:border-none resize-none bg-inherit text-text-input
max-md:h-chat-input-mobile
md:min-h-chat-input
`}
placeholder={
isMobileScreen
? Locale.Chat.Input(submitKey, isMobileScreen)
: undefined
}
onInput={(e) => onInput(e.currentTarget.value)}
value={userInput}
onKeyDown={onInputKeyDown}
onFocus={scrollToBottom}
onClick={scrollToBottom}
onPaste={handlePaste}
rows={inputRows}
autoFocus={autoFocus}
style={{
fontSize: config.fontSize,
}}
/>
{!isMobileScreen && (
<div className="flex items-center justify-center gap-3 text-sm">
<div className="flex-1">&nbsp;</div>
<div className="text-text-chat-input-placeholder font-common line-clamp-1">
{Locale.Chat.Input(submitKey)}
</div>
<Btn
className="min-w-[77px]"
icon={<SendIcon />}
text={Locale.Chat.Send}
disabled={!userInput.length}
type="primary"
onClick={() => doSubmit(userInput)}
/>
</div>
)}
</label>
</div>
</div>
);
},
);

View File

@@ -0,0 +1,248 @@
import { Fragment, useEffect, useMemo } from "react";
import { ChatMessage, useChatStore } from "@/app/store/chat";
import { CHAT_PAGE_SIZE } from "@/app/constant";
import Locale from "@/app/locales";
import { getMessageTextContent, selectOrCopy } from "@/app/utils";
import LoadingIcon from "@/app/icons/three-dots.svg";
import { Avatar } from "@/app/components/emoji";
import { MaskAvatar } from "@/app/components/mask";
import { useAppConfig } from "@/app/store/config";
import ClearContextDivider from "./ClearContextDivider";
import dynamic from "next/dynamic";
import useRelativePosition, {
Orientation,
} from "@/app/hooks/useRelativePosition";
import MessageActions, { RenderMessage } from "./MessageActions";
import Imgs from "@/app/components/Imgs";
export type { RenderMessage };
export interface ChatMessagePanelProps {
scrollRef: React.RefObject<HTMLDivElement>;
inputRef: React.RefObject<HTMLTextAreaElement>;
isMobileScreen: boolean;
msgRenderIndex: number;
userInput: string;
context: any[];
renderMessages: RenderMessage[];
scrollDomToBottom: () => void;
setAutoScroll?: (value: boolean) => void;
setMsgRenderIndex?: (newIndex: number) => void;
setHitBottom?: (value: boolean) => void;
setUserInput?: (v: string) => void;
setIsLoading?: (value: boolean) => void;
setShowPromptModal?: (value: boolean) => void;
}
let MarkdownLoadedCallback: () => void;
const Markdown = dynamic(
async () => {
const bundle = await import("@/app/components/markdown");
if (MarkdownLoadedCallback) {
MarkdownLoadedCallback();
}
return bundle.Markdown;
},
{
loading: () => <LoadingIcon />,
},
);
export default function ChatMessagePanel(props: ChatMessagePanelProps) {
const {
scrollRef,
inputRef,
setAutoScroll,
setMsgRenderIndex,
isMobileScreen,
msgRenderIndex,
setHitBottom,
setUserInput,
userInput,
context,
renderMessages,
setIsLoading,
setShowPromptModal,
scrollDomToBottom,
} = props;
const chatStore = useChatStore();
const session = chatStore.currentSession();
const config = useAppConfig();
const fontSize = config.fontSize;
const { position, getRelativePosition } = useRelativePosition({
containerRef: scrollRef,
delay: 0,
offsetDistance: 20,
});
// clear context index = context length + index in messages
const clearContextIndex =
(session.clearContextIndex ?? -1) >= 0
? session.clearContextIndex! + context.length - msgRenderIndex
: -1;
useEffect(() => {
if (!MarkdownLoadedCallback) {
MarkdownLoadedCallback = () => {
window.setTimeout(scrollDomToBottom, 100);
};
}
}, [scrollDomToBottom]);
const messages = useMemo(() => {
const endRenderIndex = Math.min(
msgRenderIndex + 3 * CHAT_PAGE_SIZE,
renderMessages.length,
);
return renderMessages.slice(msgRenderIndex, endRenderIndex);
}, [msgRenderIndex, renderMessages]);
const onChatBodyScroll = (e: HTMLElement) => {
const bottomHeight = e.scrollTop + e.clientHeight;
const edgeThreshold = e.clientHeight;
const isTouchTopEdge = e.scrollTop <= edgeThreshold;
const isTouchBottomEdge = bottomHeight >= e.scrollHeight - edgeThreshold;
const isHitBottom =
bottomHeight >= e.scrollHeight - (isMobileScreen ? 4 : 10);
const prevPageMsgIndex = msgRenderIndex - CHAT_PAGE_SIZE;
const nextPageMsgIndex = msgRenderIndex + CHAT_PAGE_SIZE;
if (isTouchTopEdge && !isTouchBottomEdge) {
setMsgRenderIndex?.(prevPageMsgIndex);
} else if (isTouchBottomEdge) {
setMsgRenderIndex?.(nextPageMsgIndex);
}
setHitBottom?.(isHitBottom);
setAutoScroll?.(isHitBottom);
};
const onRightClick = (e: any, message: ChatMessage) => {
// copy to clipboard
if (selectOrCopy(e.currentTarget, getMessageTextContent(message))) {
if (userInput.length === 0) {
setUserInput?.(getMessageTextContent(message));
}
e.preventDefault();
}
};
return (
<div
className={`pt-[80px] relative flex-1 overscroll-y-none overflow-y-auto overflow-x-hidden px-3 pb-6 md:bg-chat-panel-message bg-chat-panel-message-mobile`}
ref={scrollRef}
onScroll={(e) => onChatBodyScroll(e.currentTarget)}
onMouseDown={() => inputRef.current?.blur()}
onTouchStart={() => {
inputRef.current?.blur();
setAutoScroll?.(false);
}}
>
{messages.map((message, i) => {
const isUser = message.role === "user";
const isContext = i < context.length;
const shouldShowClearContextDivider = i === clearContextIndex - 1;
const actionsBarPosition =
position?.id === message.id &&
position?.poi.overlapPositions[Orientation.bottom]
? "bottom-[calc(100%-0.25rem)]"
: "top-[calc(100%-0.25rem)]";
return (
<Fragment key={message.id}>
<div
className={`flex mt-6 gap-2 ${isUser ? "flex-row-reverse" : ""}`}
>
<div className={`relative flex-0`}>
{isUser ? (
<Avatar avatar={config.avatar} />
) : (
<>
{["system"].includes(message.role) ? (
<Avatar avatar="2699-fe0f" />
) : (
<MaskAvatar
avatar={session.mask.avatar}
model={message.model || session.mask.modelConfig.model}
/>
)}
</>
)}
</div>
<div
className={`group relative flex ${
isUser ? "flex-row-reverse" : ""
}`}
>
<div
className={` pointer-events-none text-text-chat-message-date text-right font-common whitespace-nowrap transition-all duration-500 text-sm absolute z-1 ${
isUser ? "right-0" : "left-0"
} bottom-[100%] hidden group-hover:block`}
>
{isContext
? Locale.Chat.IsContext
: message.date.toLocaleString()}
</div>
<div
className={`transition-all duration-300 select-text break-words font-common text-sm-title ${
isUser
? "rounded-user-message bg-chat-panel-message-user"
: "rounded-bot-message bg-chat-panel-message-bot"
} box-border peer py-2 px-3`}
onPointerMoveCapture={(e) =>
getRelativePosition(e.currentTarget, message.id)
}
>
<Markdown
content={getMessageTextContent(message)}
loading={
(message.preview || message.streaming) &&
message.content.length === 0 &&
!isUser
}
onContextMenu={(e) => onRightClick(e, message)}
onDoubleClickCapture={() => {
if (!isMobileScreen) return;
setUserInput?.(getMessageTextContent(message));
}}
fontSize={fontSize}
parentRef={scrollRef}
defaultShow={i >= messages.length - 6}
className={`leading-6 max-w-message-width ${
isUser
? " text-text-chat-message-markdown-user"
: "text-text-chat-message-markdown-bot"
}`}
/>
<Imgs message={message} />
</div>
<MessageActions
className={actionsBarPosition}
message={message}
inputRef={inputRef}
isUser={isUser}
isContext={isContext}
setIsLoading={setIsLoading}
setShowPromptModal={setShowPromptModal}
/>
</div>
</div>
{shouldShowClearContextDivider && <ClearContextDivider />}
</Fragment>
);
})}
</div>
);
}

View File

@@ -0,0 +1,46 @@
import { useChatStore } from "@/app/store/chat";
import Locale from "@/app/locales";
import { useAppConfig } from "@/app/store";
export default function ClearContextDivider() {
const chatStore = useChatStore();
const { isMobileScreen } = useAppConfig();
return (
<div
className={`mt-6 mb-8 flex items-center justify-center gap-2.5 max-md:cursor-pointer`}
onClick={() => {
if (!isMobileScreen) {
return;
}
chatStore.updateCurrentSession(
(session) => (session.clearContextIndex = undefined),
);
}}
>
<div className="bg-chat-panel-message-clear-divider h-[1px] w-10"> </div>
<div className="flex items-center justify-between gap-1 text-sm">
<div className={`text-text-chat-panel-message-clear`}>
{Locale.Context.Clear}
</div>
<div
className={`
text-text-chat-panel-message-clear-revert underline font-common
md:cursor-pointer
`}
onClick={() => {
if (isMobileScreen) {
return;
}
chatStore.updateCurrentSession(
(session) => (session.clearContextIndex = undefined),
);
}}
>
{Locale.Context.Revert}
</div>
</div>
<div className="bg-chat-panel-message-clear-divider h-[1px] w-10"> </div>
</div>
);
}

View File

@@ -0,0 +1,75 @@
import { useState } from "react";
import { useChatStore } from "@/app/store/chat";
import { List, ListItem, Modal } from "@/app/components/ui-lib";
import Locale from "@/app/locales";
import { IconButton } from "@/app/components/button";
import { ContextPrompts } from "@/app/components/mask";
import CancelIcon from "@/app/icons/cancel.svg";
import ConfirmIcon from "@/app/icons/confirm.svg";
import Input from "@/app/components/Input";
export function EditMessageModal(props: { onClose: () => void }) {
const chatStore = useChatStore();
const session = chatStore.currentSession();
const [messages, setMessages] = useState(session.messages.slice());
return (
<div className="modal-mask">
<Modal
title={Locale.Chat.EditMessage.Title}
onClose={props.onClose}
actions={[
<IconButton
text={Locale.UI.Cancel}
icon={<CancelIcon />}
key="cancel"
onClick={() => {
props.onClose();
}}
/>,
<IconButton
type="primary"
text={Locale.UI.Confirm}
icon={<ConfirmIcon />}
key="ok"
onClick={() => {
chatStore.updateCurrentSession(
(session) => (session.messages = messages),
);
props.onClose();
}}
/>,
]}
// className="!bg-modal-mask"
>
<List>
<ListItem
title={Locale.Chat.EditMessage.Topic.Title}
subTitle={Locale.Chat.EditMessage.Topic.SubTitle}
>
<Input
type="text"
value={session.topic}
onChange={(e) =>
chatStore.updateCurrentSession(
(session) => (session.topic = e || ""),
)
}
className=" text-center"
></Input>
</ListItem>
</List>
<ContextPrompts
context={messages}
updateContext={(updater) => {
const newMessages = messages.slice();
updater(newMessages);
setMessages(newMessages);
}}
/>
</Modal>
</div>
);
}

View File

@@ -0,0 +1,295 @@
import Locale from "@/app/locales";
import StopIcon from "@/app/icons/pause.svg";
import DeleteRequestIcon from "@/app/icons/deleteRequestIcon.svg";
import RetryRequestIcon from "@/app/icons/retryRequestIcon.svg";
import CopyRequestIcon from "@/app/icons/copyRequestIcon.svg";
import EditRequestIcon from "@/app/icons/editRequestIcon.svg";
import PinRequestIcon from "@/app/icons/pinRequestIcon.svg";
import { showPrompt, showToast } from "@/app/components/ui-lib";
import {
copyToClipboard,
getMessageImages,
getMessageTextContent,
} from "@/app/utils";
import { MultimodalContent } from "@/app/client/api";
import { ChatMessage, useChatStore } from "@/app/store/chat";
import ActionsBar from "@/app/components/ActionsBar";
import { ChatControllerPool } from "@/app/client/controller";
import { RefObject } from "react";
export type RenderMessage = ChatMessage & { preview?: boolean };
export interface MessageActionsProps {
message: RenderMessage;
isUser: boolean;
isContext: boolean;
showActions?: boolean;
inputRef: RefObject<HTMLTextAreaElement>;
className?: string;
setIsLoading?: (value: boolean) => void;
setShowPromptModal?: (value: boolean) => void;
}
const genActionsShema = (
message: RenderMessage,
{
onEdit,
onCopy,
onPinMessage,
onDelete,
onResend,
onUserStop,
}: Record<
| "onEdit"
| "onCopy"
| "onPinMessage"
| "onDelete"
| "onResend"
| "onUserStop",
(message: RenderMessage) => void
>,
) => {
const className =
" !p-1 hover:bg-chat-message-actions-btn-hovered !rounded-actions-bar-btn ";
return [
{
id: "Edit",
icons: <EditRequestIcon />,
title: "Edit",
className,
onClick: () => onEdit(message),
},
{
id: Locale.Chat.Actions.Copy,
icons: <CopyRequestIcon />,
title: Locale.Chat.Actions.Copy,
className,
onClick: () => onCopy(message),
},
{
id: Locale.Chat.Actions.Pin,
icons: <PinRequestIcon />,
title: Locale.Chat.Actions.Pin,
className,
onClick: () => onPinMessage(message),
},
{
id: Locale.Chat.Actions.Delete,
icons: <DeleteRequestIcon />,
title: Locale.Chat.Actions.Delete,
className,
onClick: () => onDelete(message),
},
{
id: Locale.Chat.Actions.Retry,
icons: <RetryRequestIcon />,
title: Locale.Chat.Actions.Retry,
className,
onClick: () => onResend(message),
},
{
id: Locale.Chat.Actions.Stop,
icons: <StopIcon />,
title: Locale.Chat.Actions.Stop,
className,
onClick: () => onUserStop(message),
},
];
};
enum GroupType {
"streaming" = "streaming",
"isContext" = "isContext",
"normal" = "normal",
}
const groupsTypes = {
[GroupType.streaming]: [[Locale.Chat.Actions.Stop]],
[GroupType.isContext]: [["Edit"]],
[GroupType.normal]: [
[
Locale.Chat.Actions.Retry,
"Edit",
Locale.Chat.Actions.Copy,
Locale.Chat.Actions.Pin,
Locale.Chat.Actions.Delete,
],
],
};
export default function MessageActions(props: MessageActionsProps) {
const {
className,
message,
isUser,
isContext,
showActions = true,
setIsLoading,
inputRef,
setShowPromptModal,
} = props;
const chatStore = useChatStore();
const session = chatStore.currentSession();
const deleteMessage = (msgId?: string) => {
chatStore.updateCurrentSession(
(session) =>
(session.messages = session.messages.filter((m) => m.id !== msgId)),
);
};
const onDelete = (message: ChatMessage) => {
deleteMessage(message.id);
};
const onResend = (message: ChatMessage) => {
// when it is resending a message
// 1. for a user's message, find the next bot response
// 2. for a bot's message, find the last user's input
// 3. delete original user input and bot's message
// 4. resend the user's input
const resendingIndex = session.messages.findIndex(
(m) => m.id === message.id,
);
if (resendingIndex < 0 || resendingIndex >= session.messages.length) {
console.error("[Chat] failed to find resending message", message);
return;
}
let userMessage: ChatMessage | undefined;
let botMessage: ChatMessage | undefined;
if (message.role === "assistant") {
// if it is resending a bot's message, find the user input for it
botMessage = message;
for (let i = resendingIndex; i >= 0; i -= 1) {
if (session.messages[i].role === "user") {
userMessage = session.messages[i];
break;
}
}
} else if (message.role === "user") {
// if it is resending a user's input, find the bot's response
userMessage = message;
for (let i = resendingIndex; i < session.messages.length; i += 1) {
if (session.messages[i].role === "assistant") {
botMessage = session.messages[i];
break;
}
}
}
if (userMessage === undefined) {
console.error("[Chat] failed to resend", message);
return;
}
// delete the original messages
deleteMessage(userMessage.id);
deleteMessage(botMessage?.id);
// resend the message
setIsLoading?.(true);
const textContent = getMessageTextContent(userMessage);
const images = getMessageImages(userMessage);
chatStore
.onUserInput(textContent, images)
.then(() => setIsLoading?.(false));
inputRef.current?.focus();
};
const onPinMessage = (message: ChatMessage) => {
chatStore.updateCurrentSession((session) =>
session.mask.context.push(message),
);
showToast(Locale.Chat.Actions.PinToastContent, {
text: Locale.Chat.Actions.PinToastAction,
onClick: () => {
setShowPromptModal?.(true);
},
});
};
// stop response
const onUserStop = (message: ChatMessage) => {
ChatControllerPool.stop(session.id, message.id);
};
const onEdit = async () => {
const newMessage = await showPrompt(
Locale.Chat.Actions.Edit,
getMessageTextContent(message),
10,
);
let newContent: string | MultimodalContent[] = newMessage;
const images = getMessageImages(message);
if (images.length > 0) {
newContent = [{ type: "text", text: newMessage }];
for (let i = 0; i < images.length; i++) {
newContent.push({
type: "image_url",
image_url: {
url: images[i],
},
});
}
}
chatStore.updateCurrentSession((session) => {
const m = session.mask.context
.concat(session.messages)
.find((m) => m.id === message.id);
if (m) {
m.content = newContent;
}
});
};
const onCopy = () => copyToClipboard(getMessageTextContent(message));
const groupsType = [
message.streaming && GroupType.streaming,
isContext && GroupType.isContext,
GroupType.normal,
].find((i) => i) as GroupType;
return (
showActions && (
<div
className={`
absolute z-10 w-[100%]
${isUser ? "right-0" : "left-0"}
transition-all duration-300
opacity-0
pointer-events-none
group-hover:opacity-100
group-hover:pointer-events-auto
${className}
`}
>
<ActionsBar
actionsShema={genActionsShema(message, {
onCopy,
onDelete,
onPinMessage,
onEdit,
onResend,
onUserStop,
})}
groups={groupsTypes[groupsType]}
className={`
float-right flex flex-row gap-1 p-1
bg-chat-message-actions
rounded-md
shadow-message-actions-bar
dark:bg-none
`}
/>
</div>
)
);
}

View File

@@ -0,0 +1,159 @@
import Popover from "@/app/components/Popover";
import React, { useMemo, useRef } from "react";
import useRelativePosition, {
Orientation,
} from "@/app/hooks/useRelativePosition";
import Locale from "@/app/locales";
import { useChatStore } from "@/app/store/chat";
import { useAllModels } from "@/app/utils/hooks";
import { ModelType, useAppConfig } from "@/app/store/config";
import { showToast } from "@/app/components/ui-lib";
import BottomArrow from "@/app/icons/downArrowLgIcon.svg";
import BottomArrowMobile from "@/app/icons/bottomArrow.svg";
import Modal, { TriggerProps } from "@/app/components/Modal";
import Selected from "@/app/icons/selectedIcon.svg";
const ModelSelect = () => {
const config = useAppConfig();
const { isMobileScreen } = config;
const chatStore = useChatStore();
const currentModel = chatStore.currentSession().mask.modelConfig.model;
const allModels = useAllModels();
const models = useMemo(() => {
const filteredModels = allModels.filter((m) => m.available);
const defaultModel = filteredModels.find((m) => m.isDefault);
if (defaultModel) {
const arr = [
defaultModel,
...filteredModels.filter((m) => m !== defaultModel),
];
return arr;
} else {
return filteredModels;
}
}, [allModels]);
const rootRef = useRef<HTMLDivElement>(null);
const { position, getRelativePosition } = useRelativePosition({
delay: 0,
});
const contentRef = useMemo<{ current: HTMLDivElement | null }>(() => {
return {
current: null,
};
}, []);
const selectedItemRef = useRef<HTMLDivElement>(null);
const autoScrollToSelectedModal = () => {
window.setTimeout(() => {
const distanceToParent = selectedItemRef.current?.offsetTop || 0;
const childHeight = selectedItemRef.current?.offsetHeight || 0;
const parentHeight = contentRef.current?.offsetHeight || 0;
const distanceToParentCenter =
distanceToParent + childHeight / 2 - parentHeight / 2;
if (distanceToParentCenter > 0 && contentRef.current) {
contentRef.current.scrollTop = distanceToParentCenter;
}
});
};
const content: TriggerProps["content"] = ({ close }) => (
<div
className={`flex flex-col gap-1 overflow-x-hidden relative text-sm-title`}
>
{models?.map((o) => (
<div
key={o.displayName}
className={`flex items-center px-3 py-2 gap-3 rounded-action-btn hover:bg-select-option-hovered cursor-pointer`}
onClick={() => {
close();
chatStore.updateCurrentSession((session) => {
session.mask.modelConfig.model = o.name as ModelType;
session.mask.syncGlobalConfig = false;
});
showToast(o.name);
}}
ref={currentModel === o.name ? selectedItemRef : undefined}
>
<div className={`flex-1 text-text-select`}>{o.name}</div>
<div
className={currentModel === o.name ? "opacity-100" : "opacity-0"}
>
<Selected />
</div>
</div>
))}
</div>
);
if (isMobileScreen) {
return (
<Modal.Trigger
content={(e) => (
<div className="h-[100%] overflow-y-auto" ref={contentRef}>
{content(e)}
</div>
)}
type="bottom-drawer"
onOpen={(e) => {
if (e) {
autoScrollToSelectedModal();
getRelativePosition(rootRef.current!, "");
}
}}
title={Locale.Chat.SelectModel}
headerBordered
noFooter
modelClassName="h-model-bottom-drawer"
>
<div
className="flex items-center gap-1 cursor-pointer text-text-modal-select"
ref={rootRef}
>
{currentModel}
<BottomArrowMobile />
</div>
</Modal.Trigger>
);
}
return (
<Popover
content={
<div className="max-h-chat-actions-select-model-popover overflow-y-auto">
{content({ close: () => {} })}
</div>
}
trigger="click"
noArrow
placement={
position?.poi.relativePosition[1] !== Orientation.bottom ? "lb" : "lt"
}
popoverClassName="border border-select-popover rounded-lg shadow-select-popover-shadow w-actions-popover bg-model-select-popover-panel w-[280px]"
onShow={(e) => {
if (e) {
autoScrollToSelectedModal();
getRelativePosition(rootRef.current!, "");
}
}}
getPopoverPanelRef={(ref) => (contentRef.current = ref.current)}
>
<div
className="flex items-center justify-center gap-1 cursor-pointer rounded-chat-model-select pl-3 pr-2.5 py-2 font-common leading-4 bg-chat-actions-select-model hover:bg-chat-actions-select-model-hover"
ref={rootRef}
>
<div className="line-clamp-1 max-w-chat-actions-select-model text-sm-title text-text-modal-select">
{currentModel}
</div>
<BottomArrow />
</div>
</Popover>
);
};
export default ModelSelect;

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