Compare commits

...

169 Commits
v2.7 ... v2.8.5

Author SHA1 Message Date
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
Yifei Zhang
c4921f3da7 Merge pull request #1990 from Yidadaa/bugfix-0615
fix: #1982 should not fullscreen on standlone build
2023-06-16 00:02:17 +08:00
Yidadaa
91b871ef3b feat: try to add auto updater 2023-06-15 23:55:18 +08:00
Yidadaa
698be6671c feat: enable drag area for tauri apps 2023-06-15 23:20:14 +08:00
Yidadaa
47c546fafa fix: #1982 should not fullscreen on standlone build 2023-06-15 22:34:50 +08:00
Yifei Zhang
9d1a84858c Merge pull request #1973 from Jiacheng787/patch-1
fix: updating the array using push in zustand does not actually trigger component updates
2023-06-15 14:42:33 +08:00
Jiacheng Dong
4dd5bf71ea Update chat.ts 2023-06-15 13:59:38 +08:00
Yidadaa
ea253e3bae fixup: tauri app name 2023-06-15 11:01:37 +08:00
Jiacheng Dong
1d42e955fc fix: updating the array using push in zustand does not actually trigger component updates 2023-06-15 10:55:25 +08:00
Jiacheng Dong
e636d486f5 fix: updating the array using push in zustand does not actually trigger component updates 2023-06-15 10:23:01 +08:00
Yifei Zhang
818cc545eb Merge pull request #1970 from Yidadaa/app
feat: update app release workflow
2023-06-15 03:32:18 +08:00
Yidadaa
e7858495e6 feat: update app release workflow 2023-06-15 03:28:53 +08:00
Yifei Zhang
af3a273a56 Merge pull request #1969 from Yidadaa/app
feat: ready to release desktop app
2023-06-15 02:53:42 +08:00
Yidadaa
6264c02543 feat: add app logo and
release workflow
2023-06-15 02:49:08 +08:00
Yidadaa
80d5bfd7c0 feat: add app dev mode 2023-06-15 01:48:56 +08:00
Yifei Zhang
3fa8b0ad14 Merge pull request #1968 from Yidadaa/bugfix-0614
fix: #1954 lazy render bugs
2023-06-15 01:40:51 +08:00
Yidadaa
184a0b9481 fix: #1954 lazy render bugs 2023-06-15 01:34:21 +08:00
Yifei Zhang
4e80096ee4 Merge pull request #1967 from Yidadaa/bugfix-0614
fix: #1931 try to fix cors issues
2023-06-15 00:31:00 +08:00
Yidadaa
0fb775d71a fix: #1931 try to fix cors issues 2023-06-15 00:28:47 +08:00
Yidadaa
76fdd047e7 feat: new token count function 2023-06-15 00:14:38 +08:00
Yidadaa
8590750e4c feat: close #1960 add gpt-3.5-turbo-16k-0613 2023-06-14 23:22:59 +08:00
Yifei Zhang
590bd8e4bb Merge pull request #1946 from cesaryuan/fix-selection
fix: remove selection range when user blured
2023-06-14 15:42:24 +08:00
Cesaryuan
b4cb8c3d75 chore: translate comment to english 2023-06-14 14:40:51 +08:00
Cesaryuan
21aa015a79 fix: remove slection range when user blured 2023-06-14 14:30:08 +08:00
Yifei Zhang
dd5b9d420b Merge pull request #1928 from samelamin/fix_chat_time
fix date time bug on chat lists
2023-06-14 11:31:00 +08:00
Yifei Zhang
2593de5872 Merge pull request #1929 from suhipek/main
add new models
2023-06-14 10:29:00 +08:00
suhipek
d2ae740d5f add new models 2023-06-14 01:51:37 +08:00
samelamin
c56c6074e9 fix date time bug 2023-06-13 18:32:51 +01:00
Yifei Zhang
426ce7fd35 Merge pull request #1927 from Yidadaa/bugfix-0613
feat: #1000 ready to support client-side only
2023-06-14 00:48:32 +08:00
Yidadaa
2a191aacb7 fixup 2023-06-14 00:46:52 +08:00
Yidadaa
50cd33dbb2 feat: #1000 ready to support client-side only 2023-06-14 00:37:42 +08:00
Yifei Zhang
e6b49a60c0 Merge pull request #1921 from Yidadaa/bugfix-0612
fixup: #1762 optimize style on mobile screen
2023-06-13 03:04:38 +08:00
Yidadaa
a7e9356c16 fixup: #1762 optimize style on mobile screen 2023-06-13 03:04:09 +08:00
Yifei Zhang
58cc1c8482 Merge pull request #1919 from Yidadaa/bugfix-0612
feat: close #1762 add hover text for chat input actions
2023-06-13 02:29:56 +08:00
Yidadaa
88df4a2223 feat: close #1762 add hover text for chat input actions 2023-06-13 02:27:39 +08:00
Yifei Zhang
d046100875 Merge pull request #1917 from Yidadaa/bugfix-0612
feat: white url list for openai security
2023-06-13 00:49:15 +08:00
Yidadaa
0d4611052e feat: white url list for openai security 2023-06-13 00:39:29 +08:00
Yifei Zhang
bdb03e07fc Merge pull request #1868 from OldDream666/fix-auth-Security
修改 auth.tsx 输入框为password
2023-06-11 00:01:05 +08:00
Old Dream
1d790b9e8d 修改 auth.tsx 输入框为password 2023-06-09 12:39:42 +08:00
Yifei Zhang
24bf15af4f Merge pull request #1863 from Yidadaa/bugfix-0608
feat: just disable all ngnix buffering
2023-06-08 23:51:09 +08:00
Yidadaa
6410aa214e feat: just disable all ngnix buffer 2023-06-08 23:49:06 +08:00
Yidadaa
a023308d52 feat: #1000 client-side only and desktop app 2023-06-07 23:47:21 +08:00
Yifei Zhang
2516851056 Merge pull request #1844 from Yidadaa/bugfix-0607
feat: close #741 add auth page
2023-06-07 02:26:05 +08:00
Yidadaa
a3a77006ff fixup: #1815 req.body will be broken in edge runtime 2023-06-07 02:24:45 +08:00
Yidadaa
ebbd0128f1 feat: close #741 add auth page 2023-06-07 02:18:24 +08:00
Yifei Zhang
773641aa16 Merge pull request #1843 from Yidadaa/bugfix-0607
fix: #1685 clear context index should be recoverable
2023-06-07 01:19:46 +08:00
Yidadaa
57514e91b6 fix: #1685 clear context index should be recoverable 2023-06-07 01:18:05 +08:00
Yifei Zhang
a9d336baf5 Merge pull request #1842 from Yidadaa/bugfix-0607
fix: #1815 refuse to serve when disable gpt4
2023-06-07 01:07:17 +08:00
Yidadaa
37da759fd5 fix: #1815 refuse to serve when disable gpt4 2023-06-07 01:02:01 +08:00
Yifei Zhang
2ebc26953d Merge pull request #1823 from johnfelipe/main
translation doc to spanish
2023-06-06 14:53:51 +08:00
Yifei Zhang
c8ba72b185 Merge pull request #1829 from OldDream666/fix-docker-bug
fix docker-compose.yml bug
2023-06-06 14:51:52 +08:00
Old Dream
35fe6e2f54 fix docker-compose.yml bug 2023-06-06 07:01:04 +08:00
johnfelipe
f3d35e0ef3 translation doc to spanish 2023-06-05 08:52:23 -05:00
Yifei Zhang
68327907a9 Merge pull request #1813 from qingfengfenga/main
Fix docker-compose bug
2023-06-05 17:48:13 +08:00
qingfengfenga
44fe85b56d Fix docker-compose bug 2023-06-05 11:10:23 +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
107 changed files with 7535 additions and 1076 deletions

View File

@@ -27,3 +27,8 @@ HIDE_USER_API_KEY=
# Default: Empty # Default: Empty
# If you do not want users to use GPT-4, set this value to 1. # If you do not want users to use GPT-4, set this value to 1.
DISABLE_GPT4= DISABLE_GPT4=
# (optional)
# Default: Empty
# If you do not want users to query balance, set this value to 1.
HIDE_BALANCE_QUERY=

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"

88
.github/workflows/app.yml vendored Normal file
View File

@@ -0,0 +1,88 @@
name: Release App
on:
workflow_dispatch:
release:
types: [published]
jobs:
create-release:
permissions:
contents: write
runs-on: ubuntu-20.04
outputs:
release_id: ${{ steps.create-release.outputs.result }}
steps:
- uses: actions/checkout@v3
- name: setup node
uses: actions/setup-node@v3
with:
node-version: 16
- name: get version
run: echo "PACKAGE_VERSION=$(node -p "require('./src-tauri/tauri.conf.json').package.version")" >> $GITHUB_ENV
- name: create release
id: create-release
uses: actions/github-script@v6
with:
script: |
const { data } = await github.rest.repos.getLatestRelease({
owner: context.repo.owner,
repo: context.repo.repo,
})
return data.id
build-tauri:
needs: create-release
permissions:
contents: write
strategy:
fail-fast: false
matrix:
platform: [macos-latest, ubuntu-20.04, windows-latest]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v3
- name: setup node
uses: actions/setup-node@v3
with:
node-version: 16
- name: install Rust stable
uses: dtolnay/rust-toolchain@stable
- name: install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-20.04'
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
- name: install frontend dependencies
run: yarn install # change this to npm or pnpm depending on which one you use
- uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
with:
releaseId: ${{ needs.create-release.outputs.release_id }}
publish-release:
permissions:
contents: write
runs-on: ubuntu-20.04
needs: [create-release, build-tauri]
steps:
- name: publish release
id: publish-release
uses: actions/github-script@v6
env:
release_id: ${{ needs.create-release.outputs.release_id }}
with:
script: |
github.rest.repos.updateRelease({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: process.env.release_id,
draft: false,
prerelease: false
})

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

@@ -35,6 +35,6 @@ jobs:
- name: Sync check - name: Sync check
if: failure() if: failure()
run: | run: |
echo "::error::由于权限不足,导致同步失败(这是预期的行为),请前往仓库首页手动执行[Sync fork]。" echo "[Error] 由于上游仓库的 workflow 文件变更,导致 GitHub 自动暂停了本次自动更新,你需要手动 Sync Fork 一次详细教程请查看https://github.com/Yidadaa/ChatGPT-Next-Web/blob/main/README_CN.md#%E6%89%93%E5%BC%80%E8%87%AA%E5%8A%A8%E6%9B%B4%E6%96%B0"
echo "::error::Due to insufficient permissions, synchronization failed (as expected). Please go to the repository homepage and manually perform [Sync fork]." echo "[Error] Due to a change in the workflow file of the upstream repository, GitHub has automatically suspended the scheduled automatic update. You need to manually sync your fork. Please refer to the detailed tutorial for instructions: https://github.com/Yidadaa/ChatGPT-Next-Web#enable-automatic-updates"
exit 1 exit 1

3
.gitignore vendored
View File

@@ -41,3 +41,6 @@ dev
# docker-compose env files # docker-compose env files
.env .env
*.key
*.key.pub

View File

@@ -5,13 +5,25 @@
English / [简体中文](./README_CN.md) English / [简体中文](./README_CN.md)
One-Click to deploy well-designed ChatGPT web UI on Vercel. One-Click to get well-designed cross-platform ChatGPT web UI.
一键免费部署你的私人 ChatGPT 网页应用。 一键免费部署你的跨平台私人 ChatGPT 应用。
[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://chatgpt.nextweb.fun/) / [Desktop App](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [Buy Me a Coffee](https://www.buymeacoffee.com/yidadaa)
[网页版](https://chatgpt.nextweb.fun/) / [客户端](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [反馈](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-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%2FYidadaa%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web) [![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)
@@ -24,6 +36,8 @@ One-Click to deploy well-designed ChatGPT web UI on Vercel.
## Features ## Features
- **Deploy for free with one-click** on Vercel in under 1 minute - **Deploy for free with one-click** on Vercel in under 1 minute
- 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 stored locally in the browser - Privacy first, all data stored locally in the browser
- Markdown support: LaTex, mermaid, code highlight, etc. - Markdown support: LaTex, mermaid, code highlight, etc.
- Responsive design, dark mode and PWA - Responsive design, dark mode and PWA
@@ -39,23 +53,20 @@ 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] 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] 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) - [x] Share as image, share to ShareGPT [#1741](https://github.com/Yidadaa/ChatGPT-Next-Web/pull/1741)
- [ ] Desktop App with tauri - [x] Desktop App with tauri
- [ ] Self-host Model: support llama, alpaca, ChatGLM, BELLE etc. - [x] Self-host Model: Fully compatible with [RWKV-Runner](https://github.com/josStorer/RWKV-Runner), as well as server deployment of [LocalAI](https://github.com/go-skynet/LocalAI): llama/gpt4all/rwkv/vicuna/koala/gpt4all-j/cerebras/falcon/dolly etc.
- [ ] Plugins: support network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) - [ ] 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 ## 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.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.7 let's share conversations as image, or share to ShareGPT! - 🚀 v2.7 let's share conversations as image, or share to ShareGPT!
- 🚀 v2.8 now we have a client that runs across all platforms!
## 主要功能 ## 主要功能
- 在 1 分钟内使用 Vercel **免费一键部署** - 在 1 分钟内使用 Vercel **免费一键部署**
- 提供体积极小(~5MB的跨平台客户端Linux/Windows/MacOS, [下载地址](https://github.com/Yidadaa/ChatGPT-Next-Web/releases)
- 完整的 Markdown 支持LaTex 公式、Mermaid 流程图、代码高亮等等 - 完整的 Markdown 支持LaTex 公式、Mermaid 流程图、代码高亮等等
- 精心设计的 UI响应式设计支持深色模式支持 PWA - 精心设计的 UI响应式设计支持深色模式支持 PWA
- 极快的首屏加载速度(~100kb支持流式响应 - 极快的首屏加载速度(~100kb支持流式响应
@@ -72,20 +83,16 @@ One-Click to deploy well-designed ChatGPT web UI on Vercel.
- [x] 允许用户自行编辑内置 Prompt 列表 - [x] 允许用户自行编辑内置 Prompt 列表
- [x] 预制角色:使用预制角色快速定制新对话 [#993](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/993) - [x] 预制角色:使用预制角色快速定制新对话 [#993](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/993)
- [x] 分享为图片,分享到 ShareGPT 链接 [#1741](https://github.com/Yidadaa/ChatGPT-Next-Web/pull/1741) - [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 [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) - [ ] 插件机制,支持联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
### 不会开发的功能
- 界面文字自定义
- 用户登录、账号管理、消息云同步
## 最新动态 ## 最新动态
- 🚀 v2.0 已经发布,现在你可以使用面具功能快速创建预制对话了! 了解更多: [ChatGPT 提示词高阶技能:零次、一次和少样本提示](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138)。 - 🚀 v2.0 已经发布,现在你可以使用面具功能快速创建预制对话了! 了解更多: [ChatGPT 提示词高阶技能:零次、一次和少样本提示](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138)。
- 💡 想要更方便地随时随地使用本项目可以试下这款桌面插件https://github.com/mushan0x0/AI0x0.com - 💡 想要更方便地随时随地使用本项目可以试下这款桌面插件https://github.com/mushan0x0/AI0x0.com
- 🚀 v2.7 现在可以将会话分享为图片了,也可以分享到 ShareGPT 的在线链接。 - 🚀 v2.7 现在可以将会话分享为图片了,也可以分享到 ShareGPT 的在线链接。
- 🚀 v2.8 发布了横跨 Linux/Windows/MacOS 的体积极小的客户端。
## Get Started ## Get Started
@@ -178,6 +185,16 @@ 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. If you do not want users to use GPT-4, set this value to 1.
### `HIDE_BALANCE_QUERY` (optional)
> Default: Empty
If you do not want users to query balance, set this value to 1.
## Requirements
NodeJS >= 18, Docker >= 20
## Development ## Development
> [简体中文 > 如何进行二次开发](./README_CN.md#开发) > [简体中文 > 如何进行二次开发](./README_CN.md#开发)
@@ -188,6 +205,9 @@ Before starting development, you must create a new `.env.local` file at project
``` ```
OPENAI_API_KEY=<your api key here> OPENAI_API_KEY=<your api key here>
# if you are not able to access openai service, use this BASE_URL
BASE_URL=https://chatgpt1.nextweb.fun/api/proxy
``` ```
### Local Development ### Local Development
@@ -225,6 +245,12 @@ docker run -d -p 3000:3000 \
yidadaa/chatgpt-next-web yidadaa/chatgpt-next-web
``` ```
If your proxy needs password, use:
```shell
-e PROXY_URL="http://127.0.0.1:7890 user pass"
```
### Shell ### Shell
```shell ```shell

View File

@@ -98,9 +98,11 @@ OpenAI 接口代理 URL如果你手动配置了 openai 接口代理,请填
如果你不想让用户使用 GPT-4将此环境变量设置为 1 即可。 如果你不想让用户使用 GPT-4将此环境变量设置为 1 即可。
## 开发 ### `HIDE_BALANCE_QUERY` (可选)
> 强烈不建议在本地进行开发或者部署,由于一些技术原因,很难在本地配置好 OpenAI API 代理,除非你能保证可以直连 OpenAI 服务器 如果你不想让用户查询余额,将此环境变量设置为 1 即可
## 开发
点击下方按钮,开始二次开发: 点击下方按钮,开始二次开发:
@@ -110,6 +112,9 @@ OpenAI 接口代理 URL如果你手动配置了 openai 接口代理,请填
``` ```
OPENAI_API_KEY=<your api key here> OPENAI_API_KEY=<your api key here>
# 中国大陆用户,可以使用本项目自带的代理进行开发,你也可以自由选择其他代理地址
BASE_URL=https://chatgpt1.nextweb.fun/api/proxy
``` ```
### 本地开发 ### 本地开发
@@ -146,6 +151,12 @@ docker run -d -p 3000:3000 \
yidadaa/chatgpt-next-web yidadaa/chatgpt-next-web
``` ```
如果你的本地代理需要账号密码,可以使用:
```shell
-e PROXY_URL="http://127.0.0.1:7890 user password"
```
如果你需要指定其他环境变量,请自行在上述命令中增加 `-e 环境变量=环境变量值` 来指定。 如果你需要指定其他环境变量,请自行在上述命令中增加 `-e 环境变量=环境变量值` 来指定。
### 本地部署 ### 本地部署

175
README_ES.md Normal file
View File

@@ -0,0 +1,175 @@
<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.
### `HIDE_BALANCE_QUERY` (Opcional)
Si no desea que los usuarios consulte el saldo, 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)

View File

@@ -2,7 +2,6 @@ import { NextRequest } from "next/server";
import { getServerSideConfig } from "../config/server"; import { getServerSideConfig } from "../config/server";
import md5 from "spark-md5"; import md5 from "spark-md5";
import { ACCESS_CODE_PREFIX } from "../constant"; import { ACCESS_CODE_PREFIX } from "../constant";
import { OPENAI_URL } from "./common";
function getIP(req: NextRequest) { function getIP(req: NextRequest) {
let ip = req.ip ?? req.headers.get("x-real-ip"); let ip = req.ip ?? req.headers.get("x-real-ip");

View File

@@ -1,9 +1,10 @@
import { NextRequest } from "next/server"; import { NextRequest, NextResponse } from "next/server";
export const OPENAI_URL = "api.openai.com"; export const OPENAI_URL = "api.openai.com";
const DEFAULT_PROTOCOL = "https"; const DEFAULT_PROTOCOL = "https";
const PROTOCOL = process.env.PROTOCOL ?? DEFAULT_PROTOCOL; const PROTOCOL = process.env.PROTOCOL ?? DEFAULT_PROTOCOL;
const BASE_URL = process.env.BASE_URL ?? OPENAI_URL; const BASE_URL = process.env.BASE_URL ?? OPENAI_URL;
const DISABLE_GPT4 = !!process.env.DISABLE_GPT4;
export async function requestOpenai(req: NextRequest) { export async function requestOpenai(req: NextRequest) {
const controller = new AbortController(); const controller = new AbortController();
@@ -42,24 +43,50 @@ export async function requestOpenai(req: NextRequest) {
cache: "no-store", cache: "no-store",
method: req.method, method: req.method,
body: req.body, body: req.body,
// @ts-ignore
duplex: "half",
signal: controller.signal, signal: controller.signal,
}; };
// #1815 try to refuse gpt4 request
if (DISABLE_GPT4 && req.body) {
try {
const clonedBody = await req.text();
fetchOptions.body = clonedBody;
const jsonBody = JSON.parse(clonedBody);
if ((jsonBody?.model ?? "").includes("gpt-4")) {
return NextResponse.json(
{
error: true,
message: "you are not allowed to use gpt-4 model",
},
{
status: 403,
},
);
}
} catch (e) {
console.error("[OpenAI] gpt4 filter", e);
}
}
try { try {
const res = await fetch(fetchUrl, fetchOptions); const res = await fetch(fetchUrl, fetchOptions);
if (res.status === 401) { // to prevent browser prompt for credentials
// to prevent browser prompt for credentials const newHeaders = new Headers(res.headers);
const newHeaders = new Headers(res.headers); newHeaders.delete("www-authenticate");
newHeaders.delete("www-authenticate");
return new Response(res.body, {
status: res.status,
statusText: res.statusText,
headers: newHeaders,
});
}
return res; // to disbale ngnix buffering
newHeaders.set("X-Accel-Buffering", "no");
return new Response(res.body, {
status: res.status,
statusText: res.statusText,
headers: newHeaders,
});
} finally { } finally {
clearTimeout(timeoutId); clearTimeout(timeoutId);
} }

View File

@@ -10,6 +10,7 @@ const DANGER_CONFIG = {
needCode: serverConfig.needCode, needCode: serverConfig.needCode,
hideUserApiKey: serverConfig.hideUserApiKey, hideUserApiKey: serverConfig.hideUserApiKey,
enableGPT4: serverConfig.enableGPT4, enableGPT4: serverConfig.enableGPT4,
hideBalanceQuery: serverConfig.hideBalanceQuery,
}; };
declare global { declare global {

View File

@@ -1,14 +1,36 @@
import { OpenaiPath } from "@/app/constant";
import { prettyObject } from "@/app/utils/format"; import { prettyObject } from "@/app/utils/format";
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { auth } from "../../auth"; import { auth } from "../../auth";
import { requestOpenai } from "../../common"; import { requestOpenai } from "../../common";
const ALLOWD_PATH = new Set(Object.values(OpenaiPath));
async function handle( async function handle(
req: NextRequest, req: NextRequest,
{ params }: { params: { path: string[] } }, { params }: { params: { path: string[] } },
) { ) {
console.log("[OpenAI Route] params ", params); console.log("[OpenAI 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("[OpenAI Route] forbidden path ", subpath);
return NextResponse.json(
{
error: true,
msg: "you are not allowed to request " + subpath,
},
{
status: 403,
},
);
}
const authResult = auth(req); const authResult = auth(req);
if (authResult.error) { if (authResult.error) {
return NextResponse.json(authResult, { return NextResponse.json(authResult, {

View File

@@ -1,5 +1,6 @@
import { getClientConfig } from "../config/client";
import { ACCESS_CODE_PREFIX } from "../constant"; import { ACCESS_CODE_PREFIX } from "../constant";
import { ChatMessage, ModelConfig, ModelType, useAccessStore } from "../store"; import { ChatMessage, ModelType, useAccessStore } from "../store";
import { ChatGPTApi } from "./platforms/openai"; import { ChatGPTApi } from "./platforms/openai";
export const ROLES = ["system", "user", "assistant"] as const; export const ROLES = ["system", "user", "assistant"] as const;
@@ -42,6 +43,27 @@ export abstract class LLMApi {
abstract usage(): Promise<LLMUsage>; abstract usage(): Promise<LLMUsage>;
} }
type ProviderName = "openai" | "azure" | "claude" | "palm";
interface Model {
name: string;
provider: ProviderName;
ctxlen: number;
}
interface ChatProvider {
name: ProviderName;
apiConfig: {
baseUrl: string;
apiKey: string;
summaryModel: Model;
};
models: Model[];
chat: () => void;
usage: () => void;
}
export class ClientApi { export class ClientApi {
public llm: LLMApi; public llm: LLMApi;
@@ -72,7 +94,11 @@ export class ClientApi {
// Please do not modify this message // Please do not modify this message
console.log("[Share]", msgs); console.log("[Share]", msgs);
const res = await fetch("/sharegpt", { const clientConfig = getClientConfig();
const proxyUrl = "/sharegpt";
const rawUrl = "https://sharegpt.com/api/conversations";
const shareUrl = clientConfig?.isApp ? rawUrl : proxyUrl;
const res = await fetch(shareUrl, {
body: JSON.stringify({ body: JSON.stringify({
avatarUrl, avatarUrl,
items: msgs, items: msgs,

View File

@@ -1,4 +1,8 @@
import { REQUEST_TIMEOUT_MS } from "@/app/constant"; import {
DEFAULT_API_HOST,
OpenaiPath,
REQUEST_TIMEOUT_MS,
} from "@/app/constant";
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
import { ChatOptions, getHeaders, LLMApi, LLMUsage } from "../api"; import { ChatOptions, getHeaders, LLMApi, LLMUsage } from "../api";
@@ -10,12 +14,11 @@ import {
import { prettyObject } from "@/app/utils/format"; import { prettyObject } from "@/app/utils/format";
export class ChatGPTApi implements LLMApi { export class ChatGPTApi implements LLMApi {
public ChatPath = "v1/chat/completions";
public UsagePath = "dashboard/billing/usage";
public SubsPath = "dashboard/billing/subscription";
path(path: string): string { path(path: string): string {
let openaiUrl = useAccessStore.getState().openaiUrl; let openaiUrl = useAccessStore.getState().openaiUrl;
if (openaiUrl.length === 0) {
openaiUrl = DEFAULT_API_HOST;
}
if (openaiUrl.endsWith("/")) { if (openaiUrl.endsWith("/")) {
openaiUrl = openaiUrl.slice(0, openaiUrl.length - 1); openaiUrl = openaiUrl.slice(0, openaiUrl.length - 1);
} }
@@ -46,6 +49,7 @@ export class ChatGPTApi implements LLMApi {
model: modelConfig.model, model: modelConfig.model,
temperature: modelConfig.temperature, temperature: modelConfig.temperature,
presence_penalty: modelConfig.presence_penalty, presence_penalty: modelConfig.presence_penalty,
frequency_penalty: modelConfig.frequency_penalty,
}; };
console.log("[Request] openai payload: ", requestPayload); console.log("[Request] openai payload: ", requestPayload);
@@ -55,7 +59,7 @@ export class ChatGPTApi implements LLMApi {
options.onController?.(controller); options.onController?.(controller);
try { try {
const chatPath = this.path(this.ChatPath); const chatPath = this.path(OpenaiPath.ChatPath);
const chatPayload = { const chatPayload = {
method: "POST", method: "POST",
body: JSON.stringify(requestPayload), body: JSON.stringify(requestPayload),
@@ -177,14 +181,14 @@ export class ChatGPTApi implements LLMApi {
const [used, subs] = await Promise.all([ const [used, subs] = await Promise.all([
fetch( fetch(
this.path( this.path(
`${this.UsagePath}?start_date=${startDate}&end_date=${endDate}`, `${OpenaiPath.UsagePath}?start_date=${startDate}&end_date=${endDate}`,
), ),
{ {
method: "GET", method: "GET",
headers: getHeaders(), headers: getHeaders(),
}, },
), ),
fetch(this.path(this.SubsPath), { fetch(this.path(OpenaiPath.SubsPath), {
method: "GET", method: "GET",
headers: getHeaders(), headers: getHeaders(),
}), }),
@@ -228,3 +232,4 @@ export class ChatGPTApi implements LLMApi {
} as LLMUsage; } as LLMUsage;
} }
} }
export { OpenaiPath };

View File

@@ -1,4 +1,5 @@
import { useSearchParams } from "react-router-dom"; import { useSearchParams } from "react-router-dom";
import Locale from "./locales";
type Command = (param: string) => void; type Command = (param: string) => void;
interface Commands { interface Commands {
@@ -26,3 +27,45 @@ export function useCommand(commands: Commands = {}) {
setSearchParams(searchParams); setSearchParams(searchParams);
} }
} }
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,36 @@
.auth-page {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
width: 100%;
flex-direction: column;
.auth-logo {
transform: scale(1.4);
}
.auth-title {
font-size: 24px;
font-weight: bold;
line-height: 2;
}
.auth-tips {
font-size: 14px;
}
.auth-input {
margin: 3vh 0;
}
.auth-actions {
display: flex;
justify-content: center;
flex-direction: column;
button:not(:last-child) {
margin-bottom: 10px;
}
}
}

46
app/components/auth.tsx Normal file
View File

@@ -0,0 +1,46 @@
import styles from "./auth.module.scss";
import { IconButton } from "./button";
import { useNavigate } from "react-router-dom";
import { Path } from "../constant";
import { useAccessStore } from "../store";
import Locale from "../locales";
import BotIcon from "../icons/bot.svg";
export function AuthPage() {
const navigate = useNavigate();
const access = useAccessStore();
const goHome = () => navigate(Path.Home);
return (
<div className={styles["auth-page"]}>
<div className={`no-dark ${styles["auth-logo"]}`}>
<BotIcon />
</div>
<div className={styles["auth-title"]}>{Locale.Auth.Title}</div>
<div className={styles["auth-tips"]}>{Locale.Auth.Tips}</div>
<input
className={styles["auth-input"]}
type="password"
placeholder={Locale.Auth.Input}
value={access.accessCode}
onChange={(e) => {
access.updateCode(e.currentTarget.value);
}}
/>
<div className={styles["auth-actions"]}>
<IconButton
text={Locale.Auth.Confirm}
type="primary"
onClick={goHome}
/>
<IconButton text={Locale.Auth.Later} onClick={goHome} />
</div>
</div>
);
}

View File

@@ -27,6 +27,26 @@
fill: white !important; 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 { .shadow {
@@ -37,10 +57,6 @@
border: var(--border-in-light); border: var(--border-in-light);
} }
.icon-button:hover {
border-color: var(--primary);
}
.icon-button-icon { .icon-button-icon {
width: 16px; width: 16px;
height: 16px; height: 16px;
@@ -56,9 +72,12 @@
} }
.icon-button-text { .icon-button-text {
margin-left: 5px;
font-size: 12px; font-size: 12px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; 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"; import styles from "./button.module.scss";
export type ButtonType = "primary" | "danger" | null;
export function IconButton(props: { export function IconButton(props: {
onClick?: () => void; onClick?: () => void;
icon?: JSX.Element; icon?: JSX.Element;
type?: "primary" | "danger"; type?: ButtonType;
text?: string; text?: string;
bordered?: boolean; bordered?: boolean;
shadow?: boolean; shadow?: boolean;
className?: string; className?: string;
title?: string; title?: string;
disabled?: boolean; disabled?: boolean;
tabIndex?: number;
autoFocus?: boolean;
}) { }) {
return ( return (
<button <button
@@ -25,6 +29,8 @@ export function IconButton(props: {
title={props.title} title={props.title}
disabled={props.disabled} disabled={props.disabled}
role="button" role="button"
tabIndex={props.tabIndex}
autoFocus={props.autoFocus}
> >
{props.icon && ( {props.icon && (
<div <div

View File

@@ -17,6 +17,7 @@ import { Path } from "../constant";
import { MaskAvatar } from "./mask"; import { MaskAvatar } from "./mask";
import { Mask } from "../store/mask"; import { Mask } from "../store/mask";
import { useRef, useEffect } from "react"; import { useRef, useEffect } from "react";
import { showConfirm } from "./ui-lib";
export function ChatItem(props: { export function ChatItem(props: {
onClick?: () => void; onClick?: () => void;
@@ -72,9 +73,7 @@ export function ChatItem(props: {
<div className={styles["chat-item-count"]}> <div className={styles["chat-item-count"]}>
{Locale.ChatItem.ChatItemCount(props.count)} {Locale.ChatItem.ChatItemCount(props.count)}
</div> </div>
<div className={styles["chat-item-date"]}> <div className={styles["chat-item-date"]}>{props.time}</div>
{new Date(props.time).toLocaleString()}
</div>
</div> </div>
</> </>
)} )}
@@ -141,8 +140,11 @@ export function ChatList(props: { narrow?: boolean }) {
navigate(Path.Chat); navigate(Path.Chat);
selectSession(i); selectSession(i);
}} }}
onDelete={() => { onDelete={async () => {
if (!props.narrow || confirm(Locale.Home.DeleteChat)) { if (
!props.narrow ||
(await showConfirm(Locale.Home.DeleteChat))
) {
chatStore.deleteSession(i); chatStore.deleteSession(i);
} }
}} }}

View File

@@ -15,12 +15,39 @@
animation: slide-in ease 0.3s; animation: slide-in ease 0.3s;
box-shadow: var(--card-shadow); box-shadow: var(--card-shadow);
transition: all ease 0.3s; transition: all ease 0.3s;
margin-bottom: 10px;
align-items: center; align-items: center;
height: 16px;
width: var(--icon-width);
&:not(:last-child) { &:not(:last-child) {
margin-right: 5px; margin-right: 5px;
} }
.text {
white-space: nowrap;
padding-left: 5px;
opacity: 0;
transform: translateX(-5px);
transition: all ease 0.3s;
transition-delay: 0.1s;
pointer-events: none;
}
&:hover {
width: var(--full-width);
.text {
opacity: 1;
transform: translate(0);
}
}
.text,
.icon {
display: flex;
align-items: center;
justify-content: center;
}
} }
} }
@@ -174,3 +201,269 @@
} }
} }
} }
.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-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-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-user > .chat-message-container {
align-items: flex-end;
}
.chat-message-avatar {
margin-top: 20px;
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;
}
}
}
.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;
&:hover {
.chat-message-actions {
opacity: 1;
transform: translateY(0px);
max-width: 100%;
height: 40px;
.chat-message-action-date {
opacity: 0.3;
}
}
}
.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: translateY(10px);
opacity: 0;
height: 0;
max-width: 0;
.chat-input-actions {
display: flex;
flex-wrap: nowrap;
}
}
.chat-message-action-date {
white-space: nowrap;
transition: all ease 0.6s;
color: var(--black);
opacity: 0;
text-align: right;
margin-left: 20px;
}
}
.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 {
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;
box-sizing: border-box;
min-height: 68px;
}
.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;
}
}

View File

@@ -1,5 +1,11 @@
import { useDebouncedCallback } from "use-debounce"; import { useDebouncedCallback } from "use-debounce";
import { useState, useRef, useEffect, useLayoutEffect } from "react"; import React, {
useState,
useRef,
useEffect,
useMemo,
useCallback,
} from "react";
import SendWhiteIcon from "../icons/send-white.svg"; import SendWhiteIcon from "../icons/send-white.svg";
import BrainIcon from "../icons/brain.svg"; import BrainIcon from "../icons/brain.svg";
@@ -15,12 +21,16 @@ import MinIcon from "../icons/min.svg";
import ResetIcon from "../icons/reload.svg"; import ResetIcon from "../icons/reload.svg";
import BreakIcon from "../icons/break.svg"; import BreakIcon from "../icons/break.svg";
import SettingsIcon from "../icons/chat-settings.svg"; import SettingsIcon from "../icons/chat-settings.svg";
import DeleteIcon from "../icons/clear.svg";
import PinIcon from "../icons/pin.svg";
import EditIcon from "../icons/rename.svg";
import LightIcon from "../icons/light.svg"; import LightIcon from "../icons/light.svg";
import DarkIcon from "../icons/dark.svg"; import DarkIcon from "../icons/dark.svg";
import AutoIcon from "../icons/auto.svg"; import AutoIcon from "../icons/auto.svg";
import BottomIcon from "../icons/bottom.svg"; import BottomIcon from "../icons/bottom.svg";
import StopIcon from "../icons/pause.svg"; import StopIcon from "../icons/pause.svg";
import RobotIcon from "../icons/robot.svg";
import { import {
ChatMessage, ChatMessage,
@@ -32,6 +42,7 @@ import {
Theme, Theme,
useAppConfig, useAppConfig,
DEFAULT_TOPIC, DEFAULT_TOPIC,
ALL_MODELS,
} from "../store"; } from "../store";
import { import {
@@ -49,18 +60,18 @@ import { Prompt, usePromptStore } from "../store/prompt";
import Locale from "../locales"; import Locale from "../locales";
import { IconButton } from "./button"; import { IconButton } from "./button";
import styles from "./home.module.scss"; import styles from "./chat.module.scss";
import chatStyle from "./chat.module.scss";
import { ListItem, Modal } from "./ui-lib"; import { ListItem, Modal, showConfirm, showPrompt, showToast } from "./ui-lib";
import { useLocation, useNavigate } from "react-router-dom"; import { useLocation, useNavigate } from "react-router-dom";
import { LAST_INPUT_KEY, Path, REQUEST_TIMEOUT_MS } from "../constant"; import { LAST_INPUT_KEY, Path, REQUEST_TIMEOUT_MS } from "../constant";
import { Avatar } from "./emoji"; import { Avatar } from "./emoji";
import { MaskAvatar, MaskConfig } from "./mask"; import { MaskAvatar, MaskConfig } from "./mask";
import { useMaskStore } from "../store/mask"; import { useMaskStore } from "../store/mask";
import { useCommand } from "../command"; import { ChatCommandPrefix, useChatCommand, useCommand } from "../command";
import { prettyObject } from "../utils/format"; import { prettyObject } from "../utils/format";
import { ExportMessageModal } from "./exporter"; import { ExportMessageModal } from "./exporter";
import { getClientConfig } from "../config/client";
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, { const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
loading: () => <LoadingIcon />, loading: () => <LoadingIcon />,
@@ -83,8 +94,8 @@ export function SessionConfigModel(props: { onClose: () => void }) {
icon={<ResetIcon />} icon={<ResetIcon />}
bordered bordered
text={Locale.Chat.Config.Reset} text={Locale.Chat.Config.Reset}
onClick={() => { onClick={async () => {
if (confirm(Locale.Memory.ResetConfirm)) { if (await showConfirm(Locale.Memory.ResetConfirm)) {
chatStore.updateCurrentSession( chatStore.updateCurrentSession(
(session) => (session.memoryPrompt = ""), (session) => (session.memoryPrompt = ""),
); );
@@ -139,15 +150,15 @@ function PromptToast(props: {
const context = session.mask.context; const context = session.mask.context;
return ( return (
<div className={chatStyle["prompt-toast"]} key="prompt-toast"> <div className={styles["prompt-toast"]} key="prompt-toast">
{props.showToast && ( {props.showToast && (
<div <div
className={chatStyle["prompt-toast-inner"] + " clickable"} className={styles["prompt-toast-inner"] + " clickable"}
role="button" role="button"
onClick={() => props.setShowModal(true)} onClick={() => props.setShowModal(true)}
> >
<BrainIcon /> <BrainIcon />
<span className={chatStyle["prompt-toast-content"]}> <span className={styles["prompt-toast-content"]}>
{Locale.Context.Toast(context.length)} {Locale.Context.Toast(context.length)}
</span> </span>
</div> </div>
@@ -199,8 +210,7 @@ export function PromptHints(props: {
useEffect(() => { useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => { const onKeyDown = (e: KeyboardEvent) => {
if (noPrompts) return; if (noPrompts || e.metaKey || e.altKey || e.ctrlKey) {
if (e.metaKey || e.altKey || e.ctrlKey) {
return; return;
} }
// arrow up / down to select prompt // arrow up / down to select prompt
@@ -262,36 +272,83 @@ function ClearContextDivider() {
return ( return (
<div <div
className={chatStyle["clear-context"]} className={styles["clear-context"]}
onClick={() => onClick={() =>
chatStore.updateCurrentSession( chatStore.updateCurrentSession(
(session) => (session.clearContextIndex = -1), (session) => (session.clearContextIndex = undefined),
) )
} }
> >
<div className={chatStyle["clear-context-tips"]}> <div className={styles["clear-context-tips"]}>{Locale.Context.Clear}</div>
{Locale.Context.Clear} <div className={styles["clear-context-revert-btn"]}>
</div>
<div className={chatStyle["clear-context-revert-btn"]}>
{Locale.Context.Revert} {Locale.Context.Revert}
</div> </div>
</div> </div>
); );
} }
function ChatAction(props: {
text: string;
icon: JSX.Element;
onClick: () => void;
}) {
const iconRef = useRef<HTMLDivElement>(null);
const textRef = useRef<HTMLDivElement>(null);
const [width, setWidth] = useState({
full: 16,
icon: 16,
});
function updateWidth() {
if (!iconRef.current || !textRef.current) return;
const getWidth = (dom: HTMLDivElement) => dom.getBoundingClientRect().width;
const textWidth = getWidth(textRef.current);
const iconWidth = getWidth(iconRef.current);
setWidth({
full: textWidth + iconWidth,
icon: iconWidth,
});
}
return (
<div
className={`${styles["chat-input-action"]} clickable`}
onClick={() => {
props.onClick();
setTimeout(updateWidth, 1);
}}
onMouseEnter={updateWidth}
onTouchStart={updateWidth}
style={
{
"--icon-width": `${width.icon}px`,
"--full-width": `${width.full}px`,
} as React.CSSProperties
}
>
<div ref={iconRef} className={styles["icon"]}>
{props.icon}
</div>
<div className={styles["text"]} ref={textRef}>
{props.text}
</div>
</div>
);
}
function useScrollToBottom() { function useScrollToBottom() {
// for auto-scroll // for auto-scroll
const scrollRef = useRef<HTMLDivElement>(null); const scrollRef = useRef<HTMLDivElement>(null);
const [autoScroll, setAutoScroll] = useState(true); const [autoScroll, setAutoScroll] = useState(true);
const scrollToBottom = () => { const scrollToBottom = useCallback(() => {
const dom = scrollRef.current; const dom = scrollRef.current;
if (dom) { if (dom) {
setTimeout(() => (dom.scrollTop = dom.scrollHeight), 1); requestAnimationFrame(() => dom.scrollTo(0, dom.scrollHeight));
} }
}; }, []);
// auto scroll // auto scroll
useLayoutEffect(() => { useEffect(() => {
autoScroll && scrollToBottom(); autoScroll && scrollToBottom();
}); });
@@ -327,77 +384,93 @@ export function ChatActions(props: {
const couldStop = ChatControllerPool.hasPending(); const couldStop = ChatControllerPool.hasPending();
const stopAll = () => ChatControllerPool.stopAll(); const stopAll = () => ChatControllerPool.stopAll();
// switch model
const currentModel = chatStore.currentSession().mask.modelConfig.model;
function nextModel() {
const models = ALL_MODELS.filter((m) => m.available).map((m) => m.name);
const modelIndex = models.indexOf(currentModel);
const nextIndex = (modelIndex + 1) % models.length;
const nextModel = models[nextIndex];
chatStore.updateCurrentSession((session) => {
session.mask.modelConfig.model = nextModel;
session.mask.syncGlobalConfig = false;
});
}
return ( return (
<div className={chatStyle["chat-input-actions"]}> <div className={styles["chat-input-actions"]}>
{couldStop && ( {couldStop && (
<div <ChatAction
className={`${chatStyle["chat-input-action"]} clickable`}
onClick={stopAll} onClick={stopAll}
> text={Locale.Chat.InputActions.Stop}
<StopIcon /> icon={<StopIcon />}
</div> />
)} )}
{!props.hitBottom && ( {!props.hitBottom && (
<div <ChatAction
className={`${chatStyle["chat-input-action"]} clickable`}
onClick={props.scrollToBottom} onClick={props.scrollToBottom}
> text={Locale.Chat.InputActions.ToBottom}
<BottomIcon /> icon={<BottomIcon />}
</div> />
)} )}
{props.hitBottom && ( {props.hitBottom && (
<div <ChatAction
className={`${chatStyle["chat-input-action"]} clickable`}
onClick={props.showPromptModal} onClick={props.showPromptModal}
> text={Locale.Chat.InputActions.Settings}
<SettingsIcon /> icon={<SettingsIcon />}
</div> />
)} )}
<div <ChatAction
className={`${chatStyle["chat-input-action"]} clickable`}
onClick={nextTheme} onClick={nextTheme}
> text={Locale.Chat.InputActions.Theme[theme]}
{theme === Theme.Auto ? ( icon={
<AutoIcon /> <>
) : theme === Theme.Light ? ( {theme === Theme.Auto ? (
<LightIcon /> <AutoIcon />
) : theme === Theme.Dark ? ( ) : theme === Theme.Light ? (
<DarkIcon /> <LightIcon />
) : null} ) : theme === Theme.Dark ? (
</div> <DarkIcon />
) : null}
</>
}
/>
<div <ChatAction
className={`${chatStyle["chat-input-action"]} clickable`}
onClick={props.showPromptHints} onClick={props.showPromptHints}
> text={Locale.Chat.InputActions.Prompt}
<PromptIcon /> icon={<PromptIcon />}
</div> />
<div <ChatAction
className={`${chatStyle["chat-input-action"]} clickable`}
onClick={() => { onClick={() => {
navigate(Path.Masks); navigate(Path.Masks);
}} }}
> text={Locale.Chat.InputActions.Masks}
<MaskIcon /> icon={<MaskIcon />}
</div> />
<div <ChatAction
className={`${chatStyle["chat-input-action"]} clickable`} text={Locale.Chat.InputActions.Clear}
icon={<BreakIcon />}
onClick={() => { onClick={() => {
chatStore.updateCurrentSession((session) => { chatStore.updateCurrentSession((session) => {
if (session.clearContextIndex === session.messages.length) { if (session.clearContextIndex === session.messages.length) {
session.clearContextIndex = -1; session.clearContextIndex = undefined;
} else { } else {
session.clearContextIndex = session.messages.length; session.clearContextIndex = session.messages.length;
session.memoryPrompt = ""; // will clear memory session.memoryPrompt = ""; // will clear memory
} }
}); });
}} }}
> />
<BreakIcon />
</div> <ChatAction
onClick={nextModel}
text={currentModel}
icon={<RobotIcon />}
/>
</div> </div>
); );
} }
@@ -425,7 +498,7 @@ export function Chat() {
const navigate = useNavigate(); const navigate = useNavigate();
const onChatBodyScroll = (e: HTMLElement) => { const onChatBodyScroll = (e: HTMLElement) => {
const isTouchBottom = e.scrollTop + e.clientHeight >= e.scrollHeight - 100; const isTouchBottom = e.scrollTop + e.clientHeight >= e.scrollHeight - 10;
setHitBottom(isTouchBottom); setHitBottom(isTouchBottom);
}; };
@@ -434,18 +507,13 @@ export function Chat() {
const [promptHints, setPromptHints] = useState<Prompt[]>([]); const [promptHints, setPromptHints] = useState<Prompt[]>([]);
const onSearch = useDebouncedCallback( const onSearch = useDebouncedCallback(
(text: string) => { (text: string) => {
setPromptHints(promptStore.search(text)); const matchedPrompts = promptStore.search(text);
setPromptHints(matchedPrompts);
}, },
100, 100,
{ leading: true, trailing: true }, { leading: true, trailing: true },
); );
const onPromptSelect = (prompt: Prompt) => {
setPromptHints([]);
inputRef.current?.focus();
setTimeout(() => setUserInput(prompt.content), 60);
};
// auto grow input // auto grow input
const [inputRows, setInputRows] = useState(2); const [inputRows, setInputRows] = useState(2);
const measure = useDebouncedCallback( const measure = useDebouncedCallback(
@@ -467,6 +535,19 @@ export function Chat() {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(measure, [userInput]); useEffect(measure, [userInput]);
// chat commands shortcuts
const chatCommands = useChatCommand({
new: () => chatStore.newSession(),
newm: () => navigate(Path.NewChat),
prev: () => chatStore.nextSession(-1),
next: () => chatStore.nextSession(1),
clear: () =>
chatStore.updateCurrentSession(
(session) => (session.clearContextIndex = session.messages.length),
),
del: () => chatStore.deleteSession(chatStore.currentSessionIndex),
});
// only search prompts when user input is short // only search prompts when user input is short
const SEARCH_TEXT_LIMIT = 30; const SEARCH_TEXT_LIMIT = 30;
const onInput = (text: string) => { const onInput = (text: string) => {
@@ -476,6 +557,8 @@ export function Chat() {
// clear search results // clear search results
if (n === 0) { if (n === 0) {
setPromptHints([]); setPromptHints([]);
} else if (text.startsWith(ChatCommandPrefix)) {
setPromptHints(chatCommands.search(text));
} else if (!config.disablePromptHint && n < SEARCH_TEXT_LIMIT) { } else if (!config.disablePromptHint && n < SEARCH_TEXT_LIMIT) {
// check if need to trigger auto completion // check if need to trigger auto completion
if (text.startsWith("/")) { if (text.startsWith("/")) {
@@ -487,6 +570,13 @@ export function Chat() {
const doSubmit = (userInput: string) => { const doSubmit = (userInput: string) => {
if (userInput.trim() === "") return; if (userInput.trim() === "") return;
const matchCommand = chatCommands.match(userInput);
if (matchCommand.matched) {
setUserInput("");
setPromptHints([]);
matchCommand.invoke();
return;
}
setIsLoading(true); setIsLoading(true);
chatStore.onUserInput(userInput).then(() => setIsLoading(false)); chatStore.onUserInput(userInput).then(() => setIsLoading(false));
localStorage.setItem(LAST_INPUT_KEY, userInput); localStorage.setItem(LAST_INPUT_KEY, userInput);
@@ -496,6 +586,23 @@ export function Chat() {
setAutoScroll(true); setAutoScroll(true);
}; };
const onPromptSelect = (prompt: Prompt) => {
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);
};
// stop response // stop response
const onUserStop = (messageId: number) => { const onUserStop = (messageId: number) => {
ChatControllerPool.stop(sessionIndex, messageId); ChatControllerPool.stop(sessionIndex, messageId);
@@ -550,6 +657,10 @@ export function Chat() {
const onRightClick = (e: any, message: ChatMessage) => { const onRightClick = (e: any, message: ChatMessage) => {
// copy to clipboard // copy to clipboard
if (selectOrCopy(e.currentTarget, message.content)) { if (selectOrCopy(e.currentTarget, message.content)) {
if (userInput.length === 0) {
setUserInput(message.content);
}
e.preventDefault(); e.preventDefault();
} }
}; };
@@ -594,6 +705,24 @@ export function Chat() {
inputRef.current?.focus(); inputRef.current?.focus();
}; };
const onPinMessage = (botMessage: ChatMessage) => {
if (!botMessage.id) return;
const userMessageIndex = findLastUserIndex(botMessage.id);
if (userMessageIndex === null) return;
const userMessage = session.messages[userMessageIndex];
chatStore.updateCurrentSession((session) =>
session.mask.context.push(userMessage, botMessage),
);
showToast(Locale.Chat.Actions.PinToastContent, {
text: Locale.Chat.Actions.PinToastAction,
onClick: () => {
setShowPromptModal(true);
},
});
};
const context: RenderMessage[] = session.mask.hideContext const context: RenderMessage[] = session.mask.hideContext
? [] ? []
: session.mask.context.slice(); : session.mask.context.slice();
@@ -650,15 +779,22 @@ export function Chat() {
const [showPromptModal, setShowPromptModal] = useState(false); const [showPromptModal, setShowPromptModal] = useState(false);
const renameSession = () => { const renameSession = () => {
const newTopic = prompt(Locale.Chat.Rename, session.topic); showPrompt(Locale.Chat.Rename, session.topic).then((newTopic) => {
if (newTopic && newTopic !== session.topic) { if (newTopic && newTopic !== session.topic) {
chatStore.updateCurrentSession((session) => (session.topic = newTopic!)); chatStore.updateCurrentSession(
} (session) => (session.topic = newTopic!),
);
}
});
}; };
const clientConfig = useMemo(() => getClientConfig(), []);
const location = useLocation(); const location = useLocation();
const isChat = location.pathname === Path.Chat; const isChat = location.pathname === Path.Chat;
const autoFocus = !isMobileScreen || isChat; // only focus in chat page const autoFocus = !isMobileScreen || isChat; // only focus in chat page
const showMaxIcon = !isMobileScreen && !clientConfig?.isApp;
useCommand({ useCommand({
fill: setUserInput, fill: setUserInput,
@@ -669,10 +805,23 @@ export function Chat() {
return ( return (
<div className={styles.chat} key={session.id}> <div className={styles.chat} key={session.id}>
<div className="window-header"> <div className="window-header" data-tauri-drag-region>
<div className="window-header-title"> {isMobileScreen && (
<div className="window-actions">
<div className={"window-action-button"}>
<IconButton
icon={<ReturnIcon />}
bordered
title={Locale.Chat.Actions.ChatList}
onClick={() => navigate(Path.Home)}
/>
</div>
</div>
)}
<div className={`window-header-title ${styles["chat-body-title"]}`}>
<div <div
className={`window-header-main-title " ${styles["chat-body-title"]}`} className={`window-header-main-title ${styles["chat-body-main-title"]}`}
onClickCapture={renameSession} onClickCapture={renameSession}
> >
{!session.topic ? DEFAULT_TOPIC : session.topic} {!session.topic ? DEFAULT_TOPIC : session.topic}
@@ -682,21 +831,15 @@ export function Chat() {
</div> </div>
</div> </div>
<div className="window-actions"> <div className="window-actions">
<div className={"window-action-button" + " " + styles.mobile}> {!isMobileScreen && (
<IconButton <div className="window-action-button">
icon={<ReturnIcon />} <IconButton
bordered icon={<RenameIcon />}
title={Locale.Chat.Actions.ChatList} bordered
onClick={() => navigate(Path.Home)} onClick={renameSession}
/> />
</div> </div>
<div className="window-action-button"> )}
<IconButton
icon={<RenameIcon />}
bordered
onClick={renameSession}
/>
</div>
<div className="window-action-button"> <div className="window-action-button">
<IconButton <IconButton
icon={<ExportIcon />} icon={<ExportIcon />}
@@ -707,7 +850,7 @@ export function Chat() {
}} }}
/> />
</div> </div>
{!isMobileScreen && ( {showMaxIcon && (
<div className="window-action-button"> <div className="window-action-button">
<IconButton <IconButton
icon={config.tightBorder ? <MinIcon /> : <MaxIcon />} icon={config.tightBorder ? <MinIcon /> : <MaxIcon />}
@@ -760,6 +903,25 @@ export function Chat() {
> >
<div className={styles["chat-message-container"]}> <div className={styles["chat-message-container"]}>
<div className={styles["chat-message-avatar"]}> <div className={styles["chat-message-avatar"]}>
<div className={styles["chat-message-edit"]}>
<IconButton
icon={<EditIcon />}
onClick={async () => {
const newMessage = await showPrompt(
Locale.Chat.Actions.Edit,
message.content,
);
chatStore.updateCurrentSession((session) => {
const m = session.messages.find(
(m) => m.id === message.id,
);
if (m) {
m.content = newMessage;
}
});
}}
></IconButton>
</div>
{message.role === "user" ? ( {message.role === "user" ? (
<Avatar avatar={config.avatar} /> <Avatar avatar={config.avatar} />
) : ( ) : (
@@ -772,40 +934,6 @@ export function Chat() {
</div> </div>
)} )}
<div className={styles["chat-message-item"]}> <div className={styles["chat-message-item"]}>
{showActions && (
<div className={styles["chat-message-top-actions"]}>
{message.streaming ? (
<div
className={styles["chat-message-top-action"]}
onClick={() => onUserStop(message.id ?? i)}
>
{Locale.Chat.Actions.Stop}
</div>
) : (
<>
<div
className={styles["chat-message-top-action"]}
onClick={() => onDelete(message.id ?? i)}
>
{Locale.Chat.Actions.Delete}
</div>
<div
className={styles["chat-message-top-action"]}
onClick={() => onResend(message.id ?? i)}
>
{Locale.Chat.Actions.Retry}
</div>
</>
)}
<div
className={styles["chat-message-top-action"]}
onClick={() => copyToClipboard(message.content)}
>
{Locale.Chat.Actions.Copy}
</div>
</div>
)}
<Markdown <Markdown
content={message.content} content={message.content}
loading={ loading={
@@ -821,14 +949,56 @@ export function Chat() {
parentRef={scrollRef} parentRef={scrollRef}
defaultShow={i >= messages.length - 10} defaultShow={i >= messages.length - 10}
/> />
</div>
{!isUser && !message.preview && ( {showActions && (
<div className={styles["chat-message-actions"]}> <div className={styles["chat-message-actions"]}>
<div className={styles["chat-message-action-date"]}> <div
{message.date.toLocaleString()} className={styles["chat-input-actions"]}
style={{
marginTop: 10,
marginBottom: 0,
}}
>
{message.streaming ? (
<ChatAction
text={Locale.Chat.Actions.Stop}
icon={<StopIcon />}
onClick={() => onUserStop(message.id ?? i)}
/>
) : (
<>
<ChatAction
text={Locale.Chat.Actions.Retry}
icon={<ResetIcon />}
onClick={() => onResend(message.id ?? i)}
/>
<ChatAction
text={Locale.Chat.Actions.Delete}
icon={<DeleteIcon />}
onClick={() => onDelete(message.id ?? i)}
/>
<ChatAction
text={Locale.Chat.Actions.Pin}
icon={<PinIcon />}
onClick={() => onPinMessage(message)}
/>
<ChatAction
text={Locale.Chat.Actions.Copy}
icon={<CopyIcon />}
onClick={() => copyToClipboard(message.content)}
/>
</>
)}
</div>
<div className={styles["chat-message-action-date"]}>
{message.date.toLocaleString()}
</div>
</div> </div>
</div> )}
)} </div>
</div> </div>
</div> </div>
{shouldShowClearContextDivider && <ClearContextDivider />} {shouldShowClearContextDivider && <ClearContextDivider />}
@@ -868,6 +1038,9 @@ export function Chat() {
onBlur={() => setAutoScroll(false)} onBlur={() => setAutoScroll(false)}
rows={inputRows} rows={inputRows}
autoFocus={autoFocus} autoFocus={autoFocus}
style={{
fontSize: config.fontSize,
}}
/> />
<IconButton <IconButton
icon={<SendWhiteIcon />} icon={<SendWhiteIcon />}

View File

@@ -5,6 +5,7 @@ import ResetIcon from "../icons/reload.svg";
import { ISSUE_URL } from "../constant"; import { ISSUE_URL } from "../constant";
import Locale from "../locales"; import Locale from "../locales";
import { downloadAs } from "../utils"; import { downloadAs } from "../utils";
import { showConfirm } from "./ui-lib";
interface IErrorBoundaryState { interface IErrorBoundaryState {
hasError: boolean; hasError: boolean;
@@ -57,10 +58,11 @@ export class ErrorBoundary extends React.Component<any, IErrorBoundaryState> {
<IconButton <IconButton
icon={<ResetIcon />} icon={<ResetIcon />}
text="Clear All Data" text="Clear All Data"
onClick={() => onClick={async () => {
confirm(Locale.Settings.Actions.ConfirmClearAll) && if (await showConfirm(Locale.Settings.Danger.Reset.Confirm)) {
this.clearAndSaveData() this.clearAndSaveData();
} }
}}
bordered bordered
/> />
</div> </div>

View File

@@ -449,16 +449,16 @@ export function ImagePreviewer(props: {
</div> </div>
<div> <div>
<div className={styles["chat-info-item"]}> <div className={styles["chat-info-item"]}>
Model: {mask.modelConfig.model} {Locale.Exporter.Model}: {mask.modelConfig.model}
</div> </div>
<div className={styles["chat-info-item"]}> <div className={styles["chat-info-item"]}>
Messages: {props.messages.length} {Locale.Exporter.Messages}: {props.messages.length}
</div> </div>
<div className={styles["chat-info-item"]}> <div className={styles["chat-info-item"]}>
Topic: {session.topic} {Locale.Exporter.Topic}: {session.topic}
</div> </div>
<div className={styles["chat-info-item"]}> <div className={styles["chat-info-item"]}>
Time:{" "} {Locale.Exporter.Time}:{" "}
{new Date( {new Date(
props.messages.at(-1)?.date ?? Date.now(), props.messages.at(-1)?.date ?? Date.now(),
).toLocaleString()} ).toLocaleString()}

View File

@@ -185,7 +185,7 @@
.chat-item-delete { .chat-item-delete {
position: absolute; position: absolute;
top: 10px; top: 0;
right: 0; right: 0;
transition: all ease 0.3s; transition: all ease 0.3s;
opacity: 0; opacity: 0;
@@ -194,7 +194,7 @@
.chat-item:hover > .chat-item-delete { .chat-item:hover > .chat-item-delete {
opacity: 0.5; opacity: 0.5;
transform: translateX(-10px); transform: translateX(-4px);
} }
.chat-item:hover > .chat-item-delete:hover { .chat-item:hover > .chat-item-delete:hover {
@@ -283,15 +283,6 @@
} }
} }
.chat-item-delete {
top: 15px;
}
.chat-item:hover > .chat-item-delete {
opacity: 0.5;
right: 5px;
}
.sidebar-tail { .sidebar-tail {
flex-direction: column-reverse; flex-direction: column-reverse;
align-items: center; align-items: center;
@@ -322,243 +313,6 @@
margin-right: 15px; 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 { .loading-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -567,3 +321,7 @@
height: 100%; height: 100%;
width: 100%; width: 100%;
} }
.rtl-screen {
direction: rtl;
}

View File

@@ -15,6 +15,8 @@ import dynamic from "next/dynamic";
import { Path, SlotID } from "../constant"; import { Path, SlotID } from "../constant";
import { ErrorBoundary } from "./error"; import { ErrorBoundary } from "./error";
import { getLang } from "../locales";
import { import {
HashRouter as Router, HashRouter as Router,
Routes, Routes,
@@ -23,6 +25,8 @@ import {
} from "react-router-dom"; } from "react-router-dom";
import { SideBar } from "./sidebar"; import { SideBar } from "./sidebar";
import { useAppConfig } from "../store/config"; import { useAppConfig } from "../store/config";
import { AuthPage } from "./auth";
import { getClientConfig } from "../config/client";
export function Loading(props: { noLogo?: boolean }) { export function Loading(props: { noLogo?: boolean }) {
return ( return (
@@ -92,9 +96,14 @@ const useHasHydrated = () => {
const loadAsyncGoogleFont = () => { const loadAsyncGoogleFont = () => {
const linkEl = document.createElement("link"); const linkEl = document.createElement("link");
const proxyFontUrl = "/google-fonts";
const remoteFontUrl = "https://fonts.googleapis.com";
const googleFontUrl =
getClientConfig()?.buildMode === "export" ? remoteFontUrl : proxyFontUrl;
linkEl.rel = "stylesheet"; linkEl.rel = "stylesheet";
linkEl.href = linkEl.href =
"/google-fonts/css2?family=Noto+Sans+SC:wght@300;400;700;900&display=swap"; googleFontUrl +
"/css2?family=Noto+Sans+SC:wght@300;400;700;900&display=swap";
document.head.appendChild(linkEl); document.head.appendChild(linkEl);
}; };
@@ -102,6 +111,7 @@ function Screen() {
const config = useAppConfig(); const config = useAppConfig();
const location = useLocation(); const location = useLocation();
const isHome = location.pathname === Path.Home; const isHome = location.pathname === Path.Home;
const isAuth = location.pathname === Path.Auth;
const isMobileScreen = useMobileScreen(); const isMobileScreen = useMobileScreen();
useEffect(() => { useEffect(() => {
@@ -116,20 +126,28 @@ function Screen() {
config.tightBorder && !isMobileScreen config.tightBorder && !isMobileScreen
? styles["tight-container"] ? styles["tight-container"]
: styles.container : styles.container
}` } ${getLang() === "ar" ? styles["rtl-screen"] : ""}`
} }
> >
<SideBar className={isHome ? styles["sidebar-show"] : ""} /> {isAuth ? (
<>
<AuthPage />
</>
) : (
<>
<SideBar className={isHome ? styles["sidebar-show"] : ""} />
<div className={styles["window-content"]} id={SlotID.AppBody}> <div className={styles["window-content"]} id={SlotID.AppBody}>
<Routes> <Routes>
<Route path={Path.Home} element={<Chat />} /> <Route path={Path.Home} element={<Chat />} />
<Route path={Path.NewChat} element={<NewChat />} /> <Route path={Path.NewChat} element={<NewChat />} />
<Route path={Path.Masks} element={<MaskPage />} /> <Route path={Path.Masks} element={<MaskPage />} />
<Route path={Path.Chat} element={<Chat />} /> <Route path={Path.Chat} element={<Chat />} />
<Route path={Path.Settings} element={<Settings />} /> <Route path={Path.Settings} element={<Settings />} />
</Routes> </Routes>
</div> </div>
</>
)}
</div> </div>
); );
} }
@@ -137,6 +155,10 @@ function Screen() {
export function Home() { export function Home() {
useSwitchTheme(); useSwitchTheme();
useEffect(() => {
console.log("[Config] got config from build time", getClientConfig());
}, []);
if (!useHasHydrated()) { if (!useHasHydrated()) {
return <Loading />; return <Loading />;
} }

View File

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

View File

@@ -11,18 +11,21 @@ import mermaid from "mermaid";
import LoadingIcon from "../icons/three-dots.svg"; import LoadingIcon from "../icons/three-dots.svg";
import React from "react"; import React from "react";
import { useDebouncedCallback, useThrottledCallback } from "use-debounce";
export function Mermaid(props: { code: string; onError: () => void }) { export function Mermaid(props: { code: string }) {
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const [hasError, setHasError] = useState(false);
useEffect(() => { useEffect(() => {
if (props.code && ref.current) { if (props.code && ref.current) {
mermaid mermaid
.run({ .run({
nodes: [ref.current], nodes: [ref.current],
suppressErrors: true,
}) })
.catch((e) => { .catch((e) => {
props.onError(); setHasError(true);
console.error("[Mermaid] ", e.message); console.error("[Mermaid] ", e.message);
}); });
} }
@@ -41,10 +44,17 @@ export function Mermaid(props: { code: string; onError: () => void }) {
} }
} }
if (hasError) {
return null;
}
return ( return (
<div <div
className="no-dark" className="no-dark mermaid"
style={{ cursor: "pointer", overflow: "auto" }} style={{
cursor: "pointer",
overflow: "auto",
}}
ref={ref} ref={ref}
onClick={() => viewSvgInNewWindow()} onClick={() => viewSvgInNewWindow()}
> >
@@ -55,33 +65,40 @@ export function Mermaid(props: { code: string; onError: () => void }) {
export function PreCode(props: { children: any }) { export function PreCode(props: { children: any }) {
const ref = useRef<HTMLPreElement>(null); const ref = useRef<HTMLPreElement>(null);
const refText = ref.current?.innerText;
const [mermaidCode, setMermaidCode] = useState(""); const [mermaidCode, setMermaidCode] = useState("");
useEffect(() => { const renderMermaid = useDebouncedCallback(() => {
if (!ref.current) return; if (!ref.current) return;
const mermaidDom = ref.current.querySelector("code.language-mermaid"); const mermaidDom = ref.current.querySelector("code.language-mermaid");
if (mermaidDom) { if (mermaidDom) {
setMermaidCode((mermaidDom as HTMLElement).innerText); setMermaidCode((mermaidDom as HTMLElement).innerText);
} }
}, [props.children]); }, 600);
if (mermaidCode) { useEffect(() => {
return <Mermaid code={mermaidCode} onError={() => setMermaidCode("")} />; setTimeout(renderMermaid, 1);
} // eslint-disable-next-line react-hooks/exhaustive-deps
}, [refText]);
return ( return (
<pre ref={ref}> <>
<span {mermaidCode.length > 0 && (
className="copy-code-button" <Mermaid code={mermaidCode} key={mermaidCode} />
onClick={() => { )}
if (ref.current) { <pre ref={ref}>
const code = ref.current.innerText; <span
copyToClipboard(code); className="copy-code-button"
} onClick={() => {
}} if (ref.current) {
></span> const code = ref.current.innerText;
{props.children} copyToClipboard(code);
</pre> }
}}
></span>
{props.children}
</pre>
</>
); );
} }
@@ -127,43 +144,58 @@ export function Markdown(
) { ) {
const mdRef = useRef<HTMLDivElement>(null); const mdRef = useRef<HTMLDivElement>(null);
const renderedHeight = useRef(0); const renderedHeight = useRef(0);
const renderedWidth = useRef(0);
const inView = useRef(!!props.defaultShow); 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());
}
const parent = props.parentRef?.current; if (inView.current && md) {
const md = mdRef.current; 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,
},
);
const checkInView = () => { useEffect(() => {
if (parent && md) { props.parentRef?.current?.addEventListener("scroll", checkInView);
const parentBounds = parent.getBoundingClientRect(); checkInView();
const twoScreenHeight = Math.max(500, parentBounds.height * 2); return () =>
const mdBounds = md.getBoundingClientRect(); props.parentRef?.current?.removeEventListener("scroll", checkInView);
const parentTop = parentBounds.top - twoScreenHeight; // eslint-disable-next-line react-hooks/exhaustive-deps
const parentBottom = parentBounds.bottom + twoScreenHeight; }, []);
const isOverlap =
Math.max(parentTop, mdBounds.top) <=
Math.min(parentBottom, mdBounds.bottom);
inView.current = isOverlap;
}
if (inView.current && md) { const getSize = (x: number) => (!inView.current && x > 0 ? x : "auto");
renderedHeight.current = Math.max(
renderedHeight.current,
md.getBoundingClientRect().height,
);
}
};
setTimeout(() => checkInView(), 1);
return ( return (
<div <div
className="markdown-body" className="markdown-body"
style={{ style={{
fontSize: `${props.fontSize ?? 14}px`, fontSize: `${props.fontSize ?? 14}px`,
height: height: getSize(renderedHeight.current),
!inView.current && renderedHeight.current > 0 width: getSize(renderedWidth.current),
? renderedHeight.current direction: /[\u0600-\u06FF]/.test(props.content) ? "rtl" : "ltr",
: "auto",
}} }}
ref={mdRef} ref={mdRef}
onContextMenu={props.onContextMenu} onContextMenu={props.onContextMenu}

View File

@@ -15,7 +15,15 @@ import CopyIcon from "../icons/copy.svg";
import { DEFAULT_MASK_AVATAR, Mask, useMaskStore } from "../store/mask"; import { DEFAULT_MASK_AVATAR, Mask, useMaskStore } from "../store/mask";
import { ChatMessage, ModelConfig, useAppConfig, useChatStore } from "../store"; import { ChatMessage, ModelConfig, useAppConfig, useChatStore } from "../store";
import { ROLES } from "../client/api"; import { ROLES } from "../client/api";
import { Input, List, ListItem, Modal, Popover, Select } from "./ui-lib"; import {
Input,
List,
ListItem,
Modal,
Popover,
Select,
showConfirm,
} from "./ui-lib";
import { Avatar, AvatarPicker } from "./emoji"; import { Avatar, AvatarPicker } from "./emoji";
import Locale, { AllLangs, ALL_LANG_OPTIONS, Lang } from "../locales"; import Locale, { AllLangs, ALL_LANG_OPTIONS, Lang } from "../locales";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
@@ -125,10 +133,10 @@ export function MaskConfig(props: {
<input <input
type="checkbox" type="checkbox"
checked={props.mask.syncGlobalConfig} checked={props.mask.syncGlobalConfig}
onChange={(e) => { onChange={async (e) => {
if ( if (
e.currentTarget.checked && e.currentTarget.checked &&
confirm(Locale.Mask.Config.Sync.Confirm) (await showConfirm(Locale.Mask.Config.Sync.Confirm))
) { ) {
props.updateMask((mask) => { props.updateMask((mask) => {
mask.syncGlobalConfig = e.currentTarget.checked; mask.syncGlobalConfig = e.currentTarget.checked;
@@ -185,7 +193,12 @@ function ContextPromptItem(props: {
className={chatStyle["context-content"]} className={chatStyle["context-content"]}
rows={focusingInput ? 5 : 1} rows={focusingInput ? 5 : 1}
onFocus={() => setFocusingInput(true)} onFocus={() => setFocusingInput(true)}
onBlur={() => setFocusingInput(false)} onBlur={() => {
setFocusingInput(false);
// If the selection is not removed when the user loses focus, some
// extensions like "Translate" will always display a floating bar
window?.getSelection()?.removeAllRanges();
}}
onInput={(e) => onInput={(e) =>
props.update({ props.update({
...props.prompt, ...props.prompt,
@@ -434,8 +447,8 @@ export function MaskPage() {
<IconButton <IconButton
icon={<DeleteIcon />} icon={<DeleteIcon />}
text={Locale.Mask.Item.Delete} text={Locale.Mask.Item.Delete}
onClick={() => { onClick={async () => {
if (confirm(Locale.Mask.Item.DeleteConfirm)) { if (await showConfirm(Locale.Mask.Item.DeleteConfirm)) {
maskStore.delete(m.id); maskStore.delete(m.id);
} }
}} }}

View File

@@ -2,7 +2,7 @@ import { ALL_MODELS, ModalConfigValidator, ModelConfig } from "../store";
import Locale from "../locales"; import Locale from "../locales";
import { InputRange } from "./input-range"; import { InputRange } from "./input-range";
import { List, ListItem, Select } from "./ui-lib"; import { ListItem, Select } from "./ui-lib";
export function ModelConfigList(props: { export function ModelConfigList(props: {
modelConfig: ModelConfig; modelConfig: ModelConfig;
@@ -88,6 +88,42 @@ export function ModelConfigList(props: {
></InputRange> ></InputRange>
</ListItem> </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.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 <ListItem
title={Locale.Settings.HistoryCount.Title} title={Locale.Settings.HistoryCount.Title}
subTitle={Locale.Settings.HistoryCount.SubTitle} subTitle={Locale.Settings.HistoryCount.SubTitle}

View File

@@ -14,6 +14,7 @@ import Locale from "../locales";
import { useAppConfig, useChatStore } from "../store"; import { useAppConfig, useChatStore } from "../store";
import { MaskAvatar } from "./mask"; import { MaskAvatar } from "./mask";
import { useCommand } from "../command"; import { useCommand } from "../command";
import { showConfirm } from "./ui-lib";
function getIntersectionArea(aRect: DOMRect, bRect: DOMRect) { function getIntersectionArea(aRect: DOMRect, bRect: DOMRect) {
const xmin = Math.max(aRect.x, bRect.x); const xmin = Math.max(aRect.x, bRect.x);
@@ -125,8 +126,8 @@ export function NewChat() {
{!state?.fromHome && ( {!state?.fromHome && (
<IconButton <IconButton
text={Locale.NewChat.NotShow} text={Locale.NewChat.NotShow}
onClick={() => { onClick={async () => {
if (confirm(Locale.NewChat.ConfirmNoShow)) { if (await showConfirm(Locale.NewChat.ConfirmNoShow)) {
startChat(); startChat();
config.update( config.update(
(config) => (config.dontShowMaskSplashScreen = true), (config) => (config.dontShowMaskSplashScreen = true),

View File

@@ -1,4 +1,4 @@
import { useState, useEffect, useMemo, HTMLProps, useRef } from "react"; import { useState, useEffect, useMemo } from "react";
import styles from "./settings.module.scss"; import styles from "./settings.module.scss";
@@ -18,6 +18,7 @@ import {
PasswordInput, PasswordInput,
Popover, Popover,
Select, Select,
showConfirm,
} from "./ui-lib"; } from "./ui-lib";
import { ModelConfigList } from "./model-config"; import { ModelConfigList } from "./model-config";
@@ -45,6 +46,8 @@ import { ErrorBoundary } from "./error";
import { InputRange } from "./input-range"; import { InputRange } from "./input-range";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { Avatar, AvatarPicker } from "./emoji"; import { Avatar, AvatarPicker } from "./emoji";
import { getClientConfig } from "../config/client";
import { useSyncStore } from "../store/sync";
function EditPromptModal(props: { id: number; onClose: () => void }) { function EditPromptModal(props: { id: number; onClose: () => void }) {
const promptStore = usePromptStore(); const promptStore = usePromptStore();
@@ -197,6 +200,116 @@ function UserPromptModal(props: { onClose?: () => void }) {
); );
} }
function DangerItems() {
const chatStore = useChatStore();
const appConfig = useAppConfig();
return (
<List>
<ListItem
title={Locale.Settings.Danger.Reset.Title}
subTitle={Locale.Settings.Danger.Reset.SubTitle}
>
<IconButton
text={Locale.Settings.Danger.Reset.Action}
onClick={async () => {
if (await showConfirm(Locale.Settings.Danger.Reset.Confirm)) {
appConfig.reset();
}
}}
type="danger"
/>
</ListItem>
<ListItem
title={Locale.Settings.Danger.Clear.Title}
subTitle={Locale.Settings.Danger.Clear.SubTitle}
>
<IconButton
text={Locale.Settings.Danger.Clear.Action}
onClick={async () => {
if (await showConfirm(Locale.Settings.Danger.Clear.Confirm)) {
chatStore.clearAllData();
}
}}
type="danger"
/>
</ListItem>
</List>
);
}
function SyncItems() {
const syncStore = useSyncStore();
const webdav = syncStore.webDavConfig;
// not ready: https://github.com/Yidadaa/ChatGPT-Next-Web/issues/920#issuecomment-1609866332
return null;
return (
<List>
<ListItem
title={"上次同步:" + new Date().toLocaleString()}
subTitle={"20 次对话100 条消息200 提示词20 面具"}
>
<IconButton
icon={<ResetIcon />}
text="同步"
onClick={() => {
syncStore.check().then(console.log);
}}
/>
</ListItem>
<ListItem
title={"本地备份"}
subTitle={"20 次对话100 条消息200 提示词20 面具"}
></ListItem>
<ListItem
title={"Web Dav Server"}
subTitle={Locale.Settings.AccessCode.SubTitle}
>
<input
value={webdav.server}
type="text"
placeholder={"https://example.com"}
onChange={(e) => {
syncStore.update(
(config) => (config.server = e.currentTarget.value),
);
}}
/>
</ListItem>
<ListItem title="Web Dav User Name" subTitle="user name here">
<input
value={webdav.username}
type="text"
placeholder={"username"}
onChange={(e) => {
syncStore.update(
(config) => (config.username = e.currentTarget.value),
);
}}
/>
</ListItem>
<ListItem title="Web Dav Password" subTitle="password here">
<input
value={webdav.password}
type="text"
placeholder={"password"}
onChange={(e) => {
syncStore.update(
(config) => (config.password = e.currentTarget.value),
);
}}
/>
</ListItem>
</List>
);
}
function formatVersionDate(t: string) { function formatVersionDate(t: string) {
const d = new Date(+t); const d = new Date(+t);
const year = d.getUTCFullYear(); const year = d.getUTCFullYear();
@@ -215,7 +328,6 @@ export function Settings() {
const [showEmojiPicker, setShowEmojiPicker] = useState(false); const [showEmojiPicker, setShowEmojiPicker] = useState(false);
const config = useAppConfig(); const config = useAppConfig();
const updateConfig = config.update; const updateConfig = config.update;
const resetConfig = config.reset;
const chatStore = useChatStore(); const chatStore = useChatStore();
const updateStore = useUpdateStore(); const updateStore = useUpdateStore();
@@ -285,9 +397,12 @@ export function Settings() {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
const clientConfig = useMemo(() => getClientConfig(), []);
const showAccessCode = enabledAccessControl && !clientConfig?.isApp;
return ( return (
<ErrorBoundary> <ErrorBoundary>
<div className="window-header"> <div className="window-header" data-tauri-drag-region>
<div className="window-header-title"> <div className="window-header-title">
<div className="window-header-main-title"> <div className="window-header-main-title">
{Locale.Settings.Title} {Locale.Settings.Title}
@@ -297,36 +412,13 @@ export function Settings() {
</div> </div>
</div> </div>
<div className="window-actions"> <div className="window-actions">
<div className="window-action-button"> <div className="window-action-button"></div>
<IconButton <div className="window-action-button"></div>
icon={<ClearIcon />}
onClick={() => {
if (confirm(Locale.Settings.Actions.ConfirmClearAll)) {
chatStore.clearAllData();
}
}}
bordered
title={Locale.Settings.Actions.ClearAll}
/>
</div>
<div className="window-action-button">
<IconButton
icon={<ResetIcon />}
onClick={() => {
if (confirm(Locale.Settings.Actions.ConfirmResetAll)) {
resetConfig();
}
}}
bordered
title={Locale.Settings.Actions.ResetAll}
/>
</div>
<div className="window-action-button"> <div className="window-action-button">
<IconButton <IconButton
icon={<CloseIcon />} icon={<CloseIcon />}
onClick={() => navigate(Path.Home)} onClick={() => navigate(Path.Home)}
bordered bordered
title={Locale.Settings.Actions.Close}
/> />
</div> </div>
</div> </div>
@@ -484,7 +576,7 @@ export function Settings() {
</List> </List>
<List> <List>
{enabledAccessControl ? ( {showAccessCode ? (
<ListItem <ListItem
title={Locale.Settings.AccessCode.Title} title={Locale.Settings.AccessCode.Title}
subTitle={Locale.Settings.AccessCode.SubTitle} subTitle={Locale.Settings.AccessCode.SubTitle}
@@ -518,29 +610,47 @@ export function Settings() {
</ListItem> </ListItem>
) : null} ) : null}
<ListItem {!accessStore.hideBalanceQuery ? (
title={Locale.Settings.Usage.Title} <ListItem
subTitle={ title={Locale.Settings.Usage.Title}
showUsage subTitle={
? loadingUsage showUsage
? Locale.Settings.Usage.IsChecking ? loadingUsage
: Locale.Settings.Usage.SubTitle( ? Locale.Settings.Usage.IsChecking
usage?.used ?? "[?]", : Locale.Settings.Usage.SubTitle(
usage?.subscription ?? "[?]", usage?.used ?? "[?]",
) usage?.subscription ?? "[?]",
: Locale.Settings.Usage.NoAccess )
} : Locale.Settings.Usage.NoAccess
> }
{!showUsage || loadingUsage ? ( >
<div /> {!showUsage || loadingUsage ? (
) : ( <div />
<IconButton ) : (
icon={<ResetIcon></ResetIcon>} <IconButton
text={Locale.Settings.Usage.Check} icon={<ResetIcon></ResetIcon>}
onClick={() => checkUsage(true)} text={Locale.Settings.Usage.Check}
/> onClick={() => checkUsage(true)}
)} />
</ListItem> )}
</ListItem>
) : null}
{!accessStore.hideUserApiKey ? (
<ListItem
title={Locale.Settings.Endpoint.Title}
subTitle={Locale.Settings.Endpoint.SubTitle}
>
<input
type="text"
value={accessStore.openaiUrl}
placeholder="https://api.openai.com/"
onChange={(e) =>
accessStore.updateOpenAiUrl(e.currentTarget.value)
}
></input>
</ListItem>
) : null}
</List> </List>
<List> <List>
@@ -575,6 +685,8 @@ export function Settings() {
</ListItem> </ListItem>
</List> </List>
<SyncItems />
<List> <List>
<ModelConfigList <ModelConfigList
modelConfig={config.modelConfig} modelConfig={config.modelConfig}
@@ -589,6 +701,8 @@ export function Settings() {
{shouldShowPromptModal && ( {shouldShowPromptModal && (
<UserPromptModal onClose={() => setShowPromptModal(false)} /> <UserPromptModal onClose={() => setShowPromptModal(false)} />
)} )}
<DangerItems />
</div> </div>
</ErrorBoundary> </ErrorBoundary>
); );

View File

@@ -26,7 +26,7 @@ import {
import { Link, useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import { useMobileScreen } from "../utils"; import { useMobileScreen } from "../utils";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { showToast } from "./ui-lib"; import { showConfirm, showToast } from "./ui-lib";
const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, { const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, {
loading: () => null, loading: () => null,
@@ -37,14 +37,11 @@ function useHotKey() {
useEffect(() => { useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => { const onKeyDown = (e: KeyboardEvent) => {
if (e.metaKey || e.altKey || e.ctrlKey) { if (e.altKey || e.ctrlKey) {
const n = chatStore.sessions.length;
const limit = (x: number) => (x + n) % n;
const i = chatStore.currentSessionIndex;
if (e.key === "ArrowUp") { if (e.key === "ArrowUp") {
chatStore.selectSession(limit(i - 1)); chatStore.nextSession(-1);
} else if (e.key === "ArrowDown") { } else if (e.key === "ArrowDown") {
chatStore.selectSession(limit(i + 1)); chatStore.nextSession(1);
} }
} }
}; };
@@ -118,8 +115,10 @@ export function SideBar(props: { className?: string }) {
shouldNarrow && styles["narrow-sidebar"] shouldNarrow && styles["narrow-sidebar"]
}`} }`}
> >
<div className={styles["sidebar-header"]}> <div className={styles["sidebar-header"]} data-tauri-drag-region>
<div className={styles["sidebar-title"]}>ChatGPT Next</div> <div className={styles["sidebar-title"]} data-tauri-drag-region>
ChatGPT Next
</div>
<div className={styles["sidebar-sub-title"]}> <div className={styles["sidebar-sub-title"]}>
Build your own AI assistant. Build your own AI assistant.
</div> </div>
@@ -161,8 +160,8 @@ export function SideBar(props: { className?: string }) {
<div className={styles["sidebar-action"] + " " + styles.mobile}> <div className={styles["sidebar-action"] + " " + styles.mobile}>
<IconButton <IconButton
icon={<CloseIcon />} icon={<CloseIcon />}
onClick={() => { onClick={async () => {
if (confirm(Locale.Home.DeleteChat)) { if (await showConfirm(Locale.Home.DeleteChat)) {
chatStore.deleteSession(chatStore.currentSessionIndex); chatStore.deleteSession(chatStore.currentSessionIndex);
} }
}} }}

View File

@@ -207,7 +207,7 @@
.select-with-icon { .select-with-icon {
position: relative; position: relative;
max-width: fit-content; max-width: fit-content;
.select-with-icon-select { .select-with-icon-select {
height: 100%; height: 100%;
border: var(--border-in-light); border: var(--border-in-light);
@@ -227,4 +227,24 @@
transform: translateY(-50%); transform: translateY(-50%);
pointer-events: none; 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 90px 10px 14px;
resize: none;
outline: none;
box-sizing: border-box;
min-height: 68px;
&:focus {
border: 1px solid var(--primary);
}
}

View File

@@ -4,6 +4,10 @@ import CloseIcon from "../icons/close.svg";
import EyeIcon from "../icons/eye.svg"; import EyeIcon from "../icons/eye.svg";
import EyeOffIcon from "../icons/eye-off.svg"; import EyeOffIcon from "../icons/eye-off.svg";
import DownIcon from "../icons/down.svg"; import DownIcon from "../icons/down.svg";
import ConfirmIcon from "../icons/confirm.svg";
import CancelIcon from "../icons/cancel.svg";
import Locale from "../locales";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import React, { HTMLProps, useEffect, useState } from "react"; import React, { HTMLProps, useEffect, useState } from "react";
@@ -87,7 +91,7 @@ export function Loading() {
interface ModalProps { interface ModalProps {
title: string; title: string;
children?: JSX.Element | JSX.Element[]; children?: any;
actions?: JSX.Element[]; actions?: JSX.Element[];
onClose?: () => void; onClose?: () => void;
} }
@@ -262,3 +266,128 @@ export function Select(
</div> </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;
}) {
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)}
></textarea>
);
}
export function showPrompt(content: any, value = "") {
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 = "";
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}
></PromptInput>
</Modal>,
);
});
}

View File

@@ -1,16 +1,3 @@
const COMMIT_ID: string = (() => {
try {
const childProcess = require("child_process");
return childProcess
.execSync('git log -1 --format="%at000" --date=unix')
.toString()
.trim();
} catch (e) {
console.error("[Build Config] No git or not from git repo.");
return "unknown";
}
})();
export const getBuildConfig = () => { export const getBuildConfig = () => {
if (typeof process === "undefined") { if (typeof process === "undefined") {
throw Error( throw Error(
@@ -18,7 +5,24 @@ export const getBuildConfig = () => {
); );
} }
const COMMIT_ID: string = (() => {
try {
const childProcess = require("child_process");
return childProcess
.execSync('git log -1 --format="%at000" --date=unix')
.toString()
.trim();
} catch (e) {
console.error("[Build Config] No git or not from git repo.");
return "unknown";
}
})();
return { return {
commitId: COMMIT_ID, commitId: COMMIT_ID,
buildMode: process.env.BUILD_MODE ?? "standalone",
isApp: !!process.env.BUILD_APP,
}; };
}; };
export type BuildConfig = ReturnType<typeof getBuildConfig>;

27
app/config/client.ts Normal file
View File

@@ -0,0 +1,27 @@
import { BuildConfig, getBuildConfig } from "./build";
export function getClientConfig() {
if (typeof document !== "undefined") {
// client side
return JSON.parse(queryMeta("config")) as BuildConfig;
}
if (typeof process !== "undefined") {
// server side
return getBuildConfig();
}
}
function queryMeta(key: string, defaultValue?: string): string {
let ret: string;
if (document) {
const meta = document.head.querySelector(
`meta[name='${key}']`,
) as HTMLMetaElement;
ret = meta?.content ?? "";
} else {
ret = defaultValue ?? "";
}
return ret;
}

View File

@@ -10,6 +10,9 @@ declare global {
VERCEL?: string; VERCEL?: string;
HIDE_USER_API_KEY?: string; // disable user's api key input HIDE_USER_API_KEY?: string; // disable user's api key input
DISABLE_GPT4?: string; // allow user to use gpt-4 or not DISABLE_GPT4?: string; // allow user to use gpt-4 or not
BUILD_MODE?: "standalone" | "export";
BUILD_APP?: string; // is building desktop app
HIDE_BALANCE_QUERY?: string; // allow user to query balance or not
} }
} }
} }
@@ -44,5 +47,6 @@ export const getServerSideConfig = () => {
isVercel: !!process.env.VERCEL, isVercel: !!process.env.VERCEL,
hideUserApiKey: !!process.env.HIDE_USER_API_KEY, hideUserApiKey: !!process.env.HIDE_USER_API_KEY,
enableGPT4: !process.env.DISABLE_GPT4, enableGPT4: !process.env.DISABLE_GPT4,
hideBalanceQuery: !!process.env.HIDE_BALANCE_QUERY,
}; };
}; };

View File

@@ -6,6 +6,7 @@ export const UPDATE_URL = `${REPO_URL}#keep-updated`;
export const FETCH_COMMIT_URL = `https://api.github.com/repos/${OWNER}/${REPO}/commits?per_page=1`; 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 FETCH_TAG_URL = `https://api.github.com/repos/${OWNER}/${REPO}/tags?per_page=1`;
export const RUNTIME_CONFIG_DOM = "danger-runtime-config"; export const RUNTIME_CONFIG_DOM = "danger-runtime-config";
export const DEFAULT_API_HOST = "https://chatgpt1.nextweb.fun/api/proxy";
export enum Path { export enum Path {
Home = "/", Home = "/",
@@ -13,6 +14,7 @@ export enum Path {
Settings = "/settings", Settings = "/settings",
NewChat = "/new-chat", NewChat = "/new-chat",
Masks = "/masks", Masks = "/masks",
Auth = "/auth",
} }
export enum SlotID { export enum SlotID {
@@ -31,6 +33,7 @@ export enum StoreKey {
Mask = "mask-store", Mask = "mask-store",
Prompt = "prompt-store", Prompt = "prompt-store",
Update = "chat-update", Update = "chat-update",
Sync = "sync",
} }
export const MAX_SIDEBAR_WIDTH = 500; export const MAX_SIDEBAR_WIDTH = 500;
@@ -44,3 +47,16 @@ export const LAST_INPUT_KEY = "last-input";
export const REQUEST_TIMEOUT_MS = 60000; export const REQUEST_TIMEOUT_MS = 60000;
export const EXPORT_MESSAGE_CLASS_NAME = "export-markdown"; export const EXPORT_MESSAGE_CLASS_NAME = "export-markdown";
export const OpenaiPath = {
ChatPath: "v1/chat/completions",
UsagePath: "dashboard/billing/usage",
SubsPath: "dashboard/billing/subscription",
};
export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang
export const DEFAULT_SYSTEM_TEMPLATE = `
You are ChatGPT, a large language model trained by OpenAI.
Knowledge cutoff: 2021-09
Current model: {{model}}
Current time: {{time}}`;

6
app/global.d.ts vendored
View File

@@ -9,3 +9,9 @@ declare module "*.scss" {
} }
declare module "*.svg"; declare module "*.svg";
declare interface Window {
__TAURI__?: {
writeText(text: string): Promise<void>;
};
}

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

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><g opacity="1" transform="translate(0 0) rotate(0)"><mask id="bg-mask-0" fill="white"><use xlink:href="#path_0"></use></mask><g mask="url(#bg-mask-0)" ><path id="路径 1" style="fill:#333333; opacity:1;" d="M13.9967,8.00337c0,-0.81625 -0.1569,-1.59615 -0.4707,-2.3397c-0.30307,-0.71824 -0.73117,-1.3542 -1.2843,-1.90789c-0.55287,-0.55348 -1.18783,-0.98185 -1.9049,-1.28512c-0.74182,-0.31375 -1.51963,-0.47062 -2.33343,-0.47062c-0.81634,0 -1.59621,0.15693 -2.3396,0.47079c-0.71828,0.30325 -1.35419,0.73165 -1.90774,1.2852c-0.55355,0.55354 -0.98195,1.18945 -1.2852,1.90774c-0.31386,0.74339 -0.47079,1.52326 -0.47079,2.3396c0,0.8138 0.15687,1.59161 0.47062,2.33343c0.30327,0.71707 0.73164,1.35203 1.28512,1.9049c0.55369,0.55313 1.18965,0.98123 1.90789,1.2843c0.74355,0.3138 1.52345,0.4707 2.3397,0.4707c0.81371,0 1.59155,-0.15683 2.33353,-0.4705c0.717,-0.30313 1.35203,-0.7312 1.9051,-1.2842c0.553,-0.55307 0.98107,-1.1881 1.2842,-1.9051c0.31367,-0.74198 0.4705,-1.51982 0.4705,-2.33353zM15.33,8.00337c0,0.99387 -0.1919,1.94478 -0.5757,2.85273c-0.37067,0.87673 -0.89383,1.65297 -1.5695,2.3287c-0.67573,0.67567 -1.45197,1.19883 -2.3287,1.5695c-0.90795,0.3838 -1.85886,0.5757 -2.85273,0.5757c-0.99612,0 -1.94882,-0.19183 -2.8581,-0.5755c-0.8781,-0.3706 -1.65537,-0.89377 -2.33181,-1.5695c-0.67631,-0.6756 -1.19992,-1.45187 -1.57081,-2.3288c-0.38396,-0.90784 -0.57594,-1.85878 -0.57594,-2.85283c0,-0.99629 0.19192,-1.94903 0.57577,-2.8582c0.37081,-0.87829 0.89439,-1.6556 1.57074,-2.33195c0.67635,-0.67635 1.45367,-1.19993 2.33195,-1.57074c0.90917,-0.38385 1.86191,-0.57577 2.8582,-0.57577c0.99405,0 1.94499,0.19198 2.85283,0.57594c0.87693,0.37089 1.6532,0.8945 2.3288,1.57081c0.67573,0.67644 1.1989,1.45371 1.5695,2.33181c0.38367,0.90928 0.5755,1.86198 0.5755,2.8581z"></path><path id="路径 2" style="fill:#333333; opacity:1;" d="M5.4714,4.5286l6,6c0.03093,0.03093 0.05857,0.0646 0.0829,0.101c0.02433,0.0364 0.04487,0.07483 0.0616,0.1153c0.01673,0.0404 0.0294,0.08207 0.038,0.125c0.00853,0.04293 0.0128,0.0863 0.0128,0.1301c0,0.0438 -0.00427,0.08717 -0.0128,0.1301c-0.0086,0.04293 -0.02127,0.0846 -0.038,0.125c-0.01673,0.04047 -0.03727,0.0789 -0.0616,0.1153c-0.02433,0.0364 -0.05197,0.07007 -0.0829,0.101c-0.03093,0.03093 -0.0646,0.05857 -0.101,0.0829c-0.0364,0.02433 -0.07483,0.04487 -0.1153,0.0616c-0.0404,0.01673 -0.08207,0.0294 -0.125,0.038c-0.04293,0.00853 -0.0863,0.0128 -0.1301,0.0128c-0.0438,0 -0.08717,-0.00427 -0.1301,-0.0128c-0.04293,-0.0086 -0.0846,-0.02127 -0.125,-0.038c-0.04047,-0.01673 -0.0789,-0.03727 -0.1153,-0.0616c-0.0364,-0.02433 -0.07007,-0.05197 -0.101,-0.0829l-6,-6c-0.03095,-0.03095 -0.05859,-0.06463 -0.08291,-0.10102c-0.02432,-0.0364 -0.04486,-0.07482 -0.06161,-0.11526c-0.01675,-0.04044 -0.0294,-0.08213 -0.03794,-0.12506c-0.00854,-0.04293 -0.01281,-0.08629 -0.01281,-0.13006c0,-0.04377 0.00427,-0.08713 0.01281,-0.13006c0.00854,-0.04293 0.02119,-0.08462 0.03794,-0.12506c0.01675,-0.04045 0.03729,-0.07887 0.06161,-0.11526c0.02432,-0.0364 0.05196,-0.07007 0.08291,-0.10102c0.03095,-0.03095 0.06462,-0.05859 0.10102,-0.08291c0.03639,-0.02432 0.07481,-0.04486 0.11526,-0.06161c0.04044,-0.01675 0.08213,-0.0294 0.12506,-0.03794c0.04293,-0.00854 0.08629,-0.01281 0.13006,-0.01281c0.04377,0 0.08713,0.00427 0.13006,0.01281c0.04293,0.00854 0.08462,0.02119 0.12506,0.03794c0.04044,0.01675 0.07886,0.03729 0.11526,0.06161c0.03639,0.02432 0.07007,0.05196 0.10102,0.08291z"></path></g></g><defs><rect id="path_0" x="0" y="0" width="16" height="16" /></defs></svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

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

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><g opacity="1" transform="translate(0 0) rotate(0)"><mask id="bg-mask-0" fill="white"><use xlink:href="#path_0"></use></mask><g mask="url(#bg-mask-0)" ><path id="路径 1" style="fill:#333333; opacity:1;" d="M5.99607,12.8916c-0.03633,0.02413 -0.07466,0.04453 -0.11499,0.0612c-0.04034,0.01667 -0.08191,0.02923 -0.12471,0.0377c-0.04281,0.00853 -0.08603,0.0128 -0.12967,0.0128c-0.04364,0 -0.08686,-0.00423 -0.12966,-0.0127c-0.04281,-0.00847 -0.08438,-0.02103 -0.12472,-0.0377c-0.04034,-0.01667 -0.07867,-0.03707 -0.115,-0.0612c-0.03633,-0.0242 -0.06997,-0.0517 -0.1009,-0.0825l-3.96,-3.93998c-0.03103,-0.03087 -0.05876,-0.06448 -0.08317,-0.10081c-0.02441,-0.03634 -0.04505,-0.07471 -0.0619,-0.1151c-0.01685,-0.0404 -0.0296,-0.08206 -0.03825,-0.12497c-0.00865,-0.04291 -0.01303,-0.08626 -0.01314,-0.13003c-0.00011,-0.04377 0.00405,-0.08714 0.01248,-0.13009c0.00843,-0.04295 0.02097,-0.08467 0.03762,-0.12516c0.01665,-0.04048 0.03709,-0.07895 0.06132,-0.11541c0.02423,-0.03646 0.05178,-0.0702 0.08265,-0.10123c0.03087,-0.03103 0.06448,-0.05876 0.10081,-0.08317c0.03634,-0.02441 0.07471,-0.04505 0.11511,-0.0619c0.04039,-0.01685 0.08205,-0.0296 0.12496,-0.03825c0.04291,-0.00865 0.08626,-0.01303 0.13003,-0.01314c0.04377,-0.00011 0.08714,0.00405 0.13009,0.01248c0.04295,0.00843 0.08467,0.02097 0.12516,0.03762c0.04048,0.01665 0.07895,0.03709 0.11541,0.06132c0.03646,0.02423 0.07021,0.05178 0.10124,0.08265l3.48968,3.47207l8.23978,-8.20196c0.031,-0.03088 0.06473,-0.05844 0.1012,-0.08268c0.03647,-0.02423 0.07493,-0.04468 0.1154,-0.06134c0.04047,-0.01666 0.0822,-0.02921 0.1252,-0.03765c0.04293,-0.00844 0.0863,-0.01261 0.1301,-0.01251c0.04373,0.0001 0.08707,0.00447 0.13,0.01311c0.04293,0.00864 0.0846,0.02138 0.125,0.03822c0.0404,0.01685 0.07877,0.03747 0.1151,0.06188c0.03633,0.0244 0.06993,0.05211 0.1008,0.08314c0.0624,0.06265 0.1104,0.13486 0.144,0.21661c0.03367,0.08175 0.0504,0.16683 0.0502,0.25524c-0.0002,0.08841 -0.0173,0.17341 -0.0513,0.25501c-0.03407,0.08159 -0.08243,0.15357 -0.1451,0.21594l-8.70996,8.66999c-0.03093,0.0308 -0.06455,0.0583 -0.10087,0.0825z"></path></g></g><defs><rect id="path_0" x="0" y="0" width="16" height="16" /></defs></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

1
app/icons/pin.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.8 KiB

1
app/icons/robot.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -2,9 +2,7 @@
import "./styles/globals.scss"; import "./styles/globals.scss";
import "./styles/markdown.scss"; import "./styles/markdown.scss";
import "./styles/highlight.scss"; import "./styles/highlight.scss";
import { getBuildConfig } from "./config/build"; import { getClientConfig } from "./config/client";
const buildConfig = getBuildConfig();
export const metadata = { export const metadata = {
title: "ChatGPT Next Web", title: "ChatGPT Next Web",
@@ -32,7 +30,7 @@ export default function RootLayout({
return ( return (
<html lang="en"> <html lang="en">
<head> <head>
<meta name="version" content={buildConfig.commitId} /> <meta name="config" content={JSON.stringify(getClientConfig())} />
<link rel="manifest" href="/site.webmanifest"></link> <link rel="manifest" href="/site.webmanifest"></link>
<script src="/serviceWorkerRegister.js" defer></script> <script src="/serviceWorkerRegister.js" defer></script>
</head> </head>

290
app/locales/ar.ts Normal file
View File

@@ -0,0 +1,290 @@
import { SubmitKey } from "../store/config";
import type { PartialLocaleType } from "./index";
const ar: PartialLocaleType = {
WIP: "قريبًا...",
Error: {
Unauthorized:
"غير مصرح بالوصول، يرجى إدخال رمز الوصول [auth](/#/auth) في صفحة المصادقة.",
},
Auth: {
Title: "تحتاج إلى رمز الوصول",
Tips: "يرجى إدخال رمز الوصول أدناه",
Input: "رمز الوصول",
Confirm: "تأكيد",
Later: "لاحقًا",
},
ChatItem: {
ChatItemCount: (count: number) => `${count} رسائل`,
},
Chat: {
SubTitle: (count: number) => ` ${count} رسائل مع ChatGPT`,
Actions: {
ChatList: "الانتقال إلى قائمة الدردشة",
CompressedHistory: "ملخص ضغط ذاكرة التاريخ",
Export: "تصدير جميع الرسائل كـ Markdown",
Copy: "نسخ",
Stop: "توقف",
Retry: "إعادة المحاولة",
Delete: "حذف",
},
InputActions: {
Stop: "توقف",
ToBottom: "إلى آخر",
Theme: {
auto: "تلقائي",
light: "نمط فاتح",
dark: "نمط داكن",
},
Prompt: "الاقتراحات",
Masks: "الأقنعة",
Clear: "مسح السياق",
Settings: "الإعدادات",
},
Rename: "إعادة تسمية الدردشة",
Typing: "كتابة...",
Input: (submitKey: string) => {
var inputHints = ` اضغط على ${submitKey} للإرسال`;
if (submitKey === String(SubmitKey.Enter)) {
inputHints += "، Shift + Enter للإنشاء";
}
return inputHints + "، / للبحث في الاقتراحات";
},
Send: "إرسال",
Config: {
Reset: "إعادة التعيين إلى الإعدادات الافتراضية",
SaveAs: "حفظ كأقنعة",
},
},
Export: {
Title: "تصدير الرسائل",
Copy: "نسخ الكل",
Download: "تنزيل",
MessageFromYou: "رسالة منك",
MessageFromChatGPT: "رسالة من ChatGPT",
Share: "مشاركة على ShareGPT",
Format: {
Title: "صيغة التصدير",
SubTitle: "Markdown أو صورة PNG",
},
IncludeContext: {
Title: "تضمين السياق",
SubTitle: "تصدير اقتراحات السياق في الأقنعة أم لا",
},
Steps: {
Select: "تحديد",
Preview: "معاينة",
},
},
Select: {
Search: "بحث",
All: "تحديد الكل",
Latest: "تحديد أحدث",
Clear: "مسح",
},
Memory: {
Title: "اقتراحات الذاكرة",
EmptyContent: "لا شيء حتى الآن.",
Send: "إرسال الذاكرة",
Copy: "نسخ الذاكرة",
Reset: "إعادة التعيين",
ResetConfirm:
"سيؤدي إعادة التعيين إلى مسح سجل المحادثة الحالي والذاكرة التاريخية. هل أنت متأكد أنك تريد الاستمرار؟",
},
Home: {
NewChat: "دردشة جديدة",
DeleteChat: "هل تريد تأكيد حذف المحادثة المحددة؟",
DeleteToast: "تم حذف الدردشة",
Revert: "التراجع",
},
Settings: {
Title: "الإعدادات",
SubTitle: "جميع الإعدادات",
Lang: {
Name: "Language", // تنبيه: إذا كنت ترغب في إضافة ترجمة جديدة، يرجى عدم ترجمة هذه القيمة وتركها "Language"
All: "كل اللغات",
},
Avatar: "الصورة الرمزية",
FontSize: {
Title: "حجم الخط",
SubTitle: "ضبط حجم الخط لمحتوى الدردشة",
},
InputTemplate: {
Title: "نموذج الإدخال",
SubTitle: "سيتم ملء أحدث رسالة في هذا النموذج",
},
Update: {
Version: (x: string) => ` الإصدار: ${x}`,
IsLatest: "أحدث إصدار",
CheckUpdate: "التحقق من التحديث",
IsChecking: "جارٍ التحقق من التحديث...",
FoundUpdate: (x: string) => ` تم العثور على إصدار جديد: ${x}`,
GoToUpdate: "التحديث",
},
SendKey: "مفتاح الإرسال",
Theme: "السمة",
TightBorder: "حدود ضيقة",
SendPreviewBubble: {
Title: "عرض معاينة الـ Send",
SubTitle: "معاينة Markdown في فقاعة",
},
Mask: {
Title: "شاشة تظهر الأقنعة",
SubTitle: "عرض شاشة تظهر الأقنعة قبل بدء الدردشة الجديدة",
},
Prompt: {
Disable: {
Title: "تعطيل الاكتمال التلقائي",
SubTitle: "اكتب / لتشغيل الاكتمال التلقائي",
},
List: "قائمة الاقتراحات",
ListCount: (builtin: number, custom: number) => `
${builtin} مدمجة، ${custom} تم تعريفها من قبل المستخدم`,
Edit: "تعديل",
Modal: {
Title: "قائمة الاقتراحات",
Add: "إضافة واحدة",
Search: "البحث في الاقتراحات",
},
EditModal: {
Title: "تحرير الاقتراح",
},
},
HistoryCount: {
Title: "عدد الرسائل المرفقة",
SubTitle: "عدد الرسائل المرسلة المرفقة في كل طلب",
},
CompressThreshold: {
Title: "حد الضغط للتاريخ",
SubTitle: "سيتم الضغط إذا تجاوزت طول الرسائل غير المضغوطة الحد المحدد",
},
Token: {
Title: "مفتاح API",
SubTitle: "استخدم مفتاحك لتجاوز حد رمز الوصول",
Placeholder: "مفتاح OpenAI API",
},
Usage: {
Title: "رصيد الحساب",
SubTitle(used: any, total: any) {
return `تم استخدام $${used} من هذا الشهر، الاشتراك ${total}`;
},
IsChecking: "جارٍ التحقق...",
Check: "التحقق",
NoAccess: "أدخل مفتاح API للتحقق من الرصيد",
},
AccessCode: {
Title: "رمز الوصول",
SubTitle: "تم تمكين التحكم في الوصول",
Placeholder: "رمز الوصول المطلوب",
},
Endpoint: {
Title: "نقطة النهاية",
SubTitle: "يجب أن تبدأ نقطة النهاية المخصصة بـ http(s)://",
},
Model: "النموذج",
Temperature: {
Title: "الحرارة",
SubTitle: "قيمة أكبر تجعل الإخراج أكثر عشوائية",
},
MaxTokens: {
Title: "الحد الأقصى للرموز",
SubTitle: "الحد الأقصى لعدد الرموز المدخلة والرموز المُنشأة",
},
PresencePenalty: {
Title: "تأثير الوجود",
SubTitle: "قيمة أكبر تزيد من احتمالية التحدث عن مواضيع جديدة",
},
FrequencyPenalty: {
Title: "تأثير التكرار",
SubTitle: "قيمة أكبر تقلل من احتمالية تكرار نفس السطر",
},
},
Store: {
DefaultTopic: "محادثة جديدة",
BotHello: "مرحبًا! كيف يمكنني مساعدتك اليوم؟",
Error: "حدث خطأ ما، يرجى المحاولة مرة أخرى في وقت لاحق.",
Prompt: {
History: (content: string) => "هذا ملخص لسجل الدردشة كمراجعة: " + content,
Topic:
"يرجى إنشاء عنوان يتكون من أربع إلى خمس كلمات يلخص محادثتنا دون أي مقدمة أو ترقيم أو علامات ترقيم أو نقاط أو رموز إضافية. قم بإزالة علامات التنصيص المحيطة.",
Summarize:
"قم بتلخيص النقاش بشكل موجز في 200 كلمة أو أقل لاستخدامه كاقتراح للسياق في المستقبل.",
},
},
Copy: {
Success: "تم النسخ إلى الحافظة",
Failed: "فشلت عملية النسخ، يرجى منح الإذن للوصول إلى الحافظة",
},
Context: {
Toast: (x: any) => `مع ${x} اقتراحًا ذا سياق`,
Edit: "الاقتراحات السياقية والذاكرة",
Add: "إضافة اقتراح",
Clear: "مسح السياق",
Revert: "التراجع",
},
Plugin: {
Name: "المكوّن الإضافي",
},
Mask: {
Name: "الأقنعة",
Page: {
Title: "قالب الاقتراح",
SubTitle: (count: number) => `${count} قوالب الاقتراح`,
Search: "البحث في القوالب",
Create: "إنشاء",
},
Item: {
Info: (count: number) => `${count} اقتراحات`,
Chat: "الدردشة",
View: "عرض",
Edit: "تعديل",
Delete: "حذف",
DeleteConfirm: "تأكيد الحذف؟",
},
EditModal: {
Title: (readonly: boolean) => `
تعديل قالب الاقتراح ${readonly ? "(للقراءة فقط)" : ""}`,
Download: "تنزيل",
Clone: "استنساخ",
},
Config: {
Avatar: "صورة الروبوت",
Name: "اسم الروبوت",
Sync: {
Title: "استخدام الإعدادات العامة",
SubTitle: "استخدام الإعدادات العامة في هذه الدردشة",
Confirm: "تأكيد الاستبدال بالإعدادات المخصصة بالإعدادات العامة؟",
},
HideContext: {
Title: "إخفاء اقتراحات السياق",
SubTitle: "عدم عرض اقتراحات السياق في الدردشة",
},
},
},
NewChat: {
Return: "العودة",
Skip: "ابدأ فقط",
Title: "اختيار قناع",
SubTitle: "دردشة مع الروح وراء القناع",
More: "المزيد",
NotShow: "عدم العرض مرة أخرى",
ConfirmNoShow: "تأكيد تعطيله؟ يمكنك تمكينه في الإعدادات لاحقًا.",
},
UI: {
Confirm: "تأكيد",
Cancel: "إلغاء",
Close: "إغلاق",
Create: "إنشاء",
Edit: "تعديل",
},
Exporter: {
Model: "النموذج",
Messages: "الرسائل",
Topic: "الموضوع",
Time: "الوقت",
},
};
export default ar;

View File

@@ -4,13 +4,20 @@ const cn = {
WIP: "该功能仍在开发中……", WIP: "该功能仍在开发中……",
Error: { Error: {
Unauthorized: Unauthorized:
"访问密码不正确或为空,请前往[设置](/#/settings)页输入正确的访问密码,或者填入你自己的 OpenAI API Key。", "访问密码不正确或为空,请前往[登录](/#/auth)页输入正确的访问密码,或者在[设置](/#/settings)页填入你自己的 OpenAI API Key。",
},
Auth: {
Title: "需要密码",
Tips: "管理员开启了密码验证,请在下方填入访问码",
Input: "在此处填写访问码",
Confirm: "确认",
Later: "稍后再说",
}, },
ChatItem: { ChatItem: {
ChatItemCount: (count: number) => `${count} 条对话`, ChatItemCount: (count: number) => `${count} 条对话`,
}, },
Chat: { Chat: {
SubTitle: (count: number) => `与 ChatGPT 的 ${count} 条对话`, SubTitle: (count: number) => ` ${count} 条对话`,
Actions: { Actions: {
ChatList: "查看消息列表", ChatList: "查看消息列表",
CompressedHistory: "查看压缩后的历史 Prompt", CompressedHistory: "查看压缩后的历史 Prompt",
@@ -18,7 +25,32 @@ const cn = {
Copy: "复制", Copy: "复制",
Stop: "停止", Stop: "停止",
Retry: "重试", Retry: "重试",
Pin: "固定",
PinToastContent: "已将 2 条对话固定至预设提示词",
PinToastAction: "查看",
Delete: "删除", Delete: "删除",
Edit: "编辑",
},
Commands: {
new: "新建聊天",
newm: "从面具新建聊天",
next: "下一个聊天",
prev: "上一个聊天",
clear: "清除上下文",
del: "删除聊天",
},
InputActions: {
Stop: "停止响应",
ToBottom: "滚到最新",
Theme: {
auto: "自动主题",
light: "亮色模式",
dark: "深色模式",
},
Prompt: "快捷指令",
Masks: "所有面具",
Clear: "清除聊天",
Settings: "对话设置",
}, },
Rename: "重命名对话", Rename: "重命名对话",
Typing: "正在输入…", Typing: "正在输入…",
@@ -27,7 +59,7 @@ const cn = {
if (submitKey === String(SubmitKey.Enter)) { if (submitKey === String(SubmitKey.Enter)) {
inputHints += "Shift + Enter 换行"; inputHints += "Shift + Enter 换行";
} }
return inputHints + "/ 触发补全"; return inputHints + "/ 触发补全: 触发命令";
}, },
Send: "发送", Send: "发送",
Config: { Config: {
@@ -77,13 +109,21 @@ const cn = {
}, },
Settings: { Settings: {
Title: "设置", Title: "设置",
SubTitle: "设置选项", SubTitle: "所有设置选项",
Actions: {
ClearAll: "清除所有数据", Danger: {
ResetAll: "重置所有选项", Reset: {
Close: "关闭", Title: "重置所有设置",
ConfirmResetAll: "确认重置所有配置?", SubTitle: "重置所有设置项回默认值",
ConfirmClearAll: "确认清除所有数据?", Action: "立即重置",
Confirm: "确认重置所有设置?",
},
Clear: {
Title: "清除所有数据",
SubTitle: "清除所有聊天、设置数据",
Action: "立即清除",
Confirm: "确认清除所有聊天、设置数据?",
},
}, },
Lang: { Lang: {
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language` Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
@@ -95,6 +135,11 @@ const cn = {
SubTitle: "聊天内容的字体大小", SubTitle: "聊天内容的字体大小",
}, },
InputTemplate: {
Title: "用户输入预处理",
SubTitle: "用户最新的一条消息会填充到此模板",
},
Update: { Update: {
Version: (x: string) => `当前版本:${x}`, Version: (x: string) => `当前版本:${x}`,
IsLatest: "已是最新版本", IsLatest: "已是最新版本",
@@ -160,6 +205,10 @@ const cn = {
SubTitle: "管理员已开启加密访问", SubTitle: "管理员已开启加密访问",
Placeholder: "请输入访问密码", Placeholder: "请输入访问密码",
}, },
Endpoint: {
Title: "接口地址",
SubTitle: "除默认地址外,必须包含 http(s)://",
},
Model: "模型 (model)", Model: "模型 (model)",
Temperature: { Temperature: {
Title: "随机性 (temperature)", Title: "随机性 (temperature)",
@@ -173,6 +222,10 @@ const cn = {
Title: "话题新鲜度 (presence_penalty)", Title: "话题新鲜度 (presence_penalty)",
SubTitle: "值越大,越有可能扩展到新话题", SubTitle: "值越大,越有可能扩展到新话题",
}, },
FrequencyPenalty: {
Title: "频率惩罚度 (frequency_penalty)",
SubTitle: "值越大,越有可能降低重复字词",
},
}, },
Store: { Store: {
DefaultTopic: "新的聊天", DefaultTopic: "新的聊天",
@@ -253,6 +306,12 @@ const cn = {
Create: "新建", Create: "新建",
Edit: "编辑", Edit: "编辑",
}, },
Exporter: {
Model: "模型",
Messages: "消息",
Topic: "主题",
Time: "时间",
},
}; };
type DeepPartial<T> = T extends object type DeepPartial<T> = T extends object
@@ -260,7 +319,8 @@ type DeepPartial<T> = T extends object
[P in keyof T]?: DeepPartial<T[P]>; [P in keyof T]?: DeepPartial<T[P]>;
} }
: T; : T;
export type LocaleType = DeepPartial<typeof cn>;
export type RequiredLocaleType = typeof cn; export type LocaleType = typeof cn;
export type PartialLocaleType = DeepPartial<typeof cn>;
export default cn; export default cn;

View File

@@ -1,7 +1,7 @@
import { SubmitKey } from "../store/config"; import { SubmitKey } from "../store/config";
import type { LocaleType } from "./index"; import type { PartialLocaleType } from "./index";
const cs: LocaleType = { const cs: PartialLocaleType = {
WIP: "V přípravě...", WIP: "V přípravě...",
Error: { Error: {
Unauthorized: Unauthorized:
@@ -61,13 +61,7 @@ const cs: LocaleType = {
Settings: { Settings: {
Title: "Nastavení", Title: "Nastavení",
SubTitle: "Všechna nastavení", SubTitle: "Všechna nastavení",
Actions: {
ClearAll: "Vymazat všechna data",
ResetAll: "Obnovit veškeré nastavení",
Close: "Zavřít",
ConfirmResetAll: "Jste si jisti, že chcete obnovit všechna nastavení?",
ConfirmClearAll: "Jste si jisti, že chcete smazat všechna data?",
},
Lang: { Lang: {
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language` Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
All: "Všechny jazyky", All: "Všechny jazyky",
@@ -155,6 +149,11 @@ const cs: LocaleType = {
Title: "Přítomnostní korekce", Title: "Přítomnostní korekce",
SubTitle: "Větší hodnota zvyšuje pravděpodobnost nových témat.", SubTitle: "Větší hodnota zvyšuje pravděpodobnost nových témat.",
}, },
FrequencyPenalty: {
Title: "Frekvenční penalizace",
SubTitle:
"Větší hodnota snižující pravděpodobnost opakování stejného řádku",
},
}, },
Store: { Store: {
DefaultTopic: "Nová konverzace", DefaultTopic: "Nová konverzace",
@@ -226,6 +225,12 @@ const cs: LocaleType = {
Create: "Vytvořit", Create: "Vytvořit",
Edit: "Upravit", Edit: "Upravit",
}, },
Exporter: {
Model: "Model",
Messages: "Zprávy",
Topic: "Téma",
Time: "Čas",
},
}; };
export default cs; export default cs;

View File

@@ -1,7 +1,7 @@
import { SubmitKey } from "../store/config"; import { SubmitKey } from "../store/config";
import type { LocaleType } from "./index"; import type { PartialLocaleType } from "./index";
const de: LocaleType = { const de: PartialLocaleType = {
WIP: "In Bearbeitung...", WIP: "In Bearbeitung...",
Error: { Error: {
Unauthorized: Unauthorized:
@@ -61,14 +61,7 @@ const de: LocaleType = {
Settings: { Settings: {
Title: "Einstellungen", Title: "Einstellungen",
SubTitle: "Alle Einstellungen", SubTitle: "Alle Einstellungen",
Actions: {
ClearAll: "Alle Daten löschen",
ResetAll: "Alle Einstellungen zurücksetzen",
Close: "Schließen",
ConfirmResetAll:
"Möchten Sie wirklich alle Konfigurationen zurücksetzen?",
ConfirmClearAll: "Möchten Sie wirklich alle Chats zurücksetzen?",
},
Lang: { Lang: {
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language` Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
All: "Alle Sprachen", All: "Alle Sprachen",
@@ -158,6 +151,11 @@ const de: LocaleType = {
SubTitle: SubTitle:
"Ein größerer Wert erhöht die Wahrscheinlichkeit, dass über neue Themen gesprochen wird", "Ein größerer Wert erhöht die Wahrscheinlichkeit, dass über neue Themen gesprochen wird",
}, },
FrequencyPenalty: {
Title: "Frequency Penalty", // HäufigkeitStrafe
SubTitle:
"Ein größerer Wert, der die Wahrscheinlichkeit verringert, dass dieselbe Zeile wiederholt wird",
},
}, },
Store: { Store: {
DefaultTopic: "Neues Gespräch", DefaultTopic: "Neues Gespräch",
@@ -231,6 +229,12 @@ const de: LocaleType = {
Create: "Create", Create: "Create",
Edit: "Edit", Edit: "Edit",
}, },
Exporter: {
Model: "Modell",
Messages: "Nachrichten",
Topic: "Thema",
Time: "Zeit",
},
}; };
export default de; export default de;

View File

@@ -1,17 +1,24 @@
import { SubmitKey } from "../store/config"; import { SubmitKey } from "../store/config";
import { RequiredLocaleType } from "./index"; import { LocaleType } from "./index";
const en: RequiredLocaleType = { const en: LocaleType = {
WIP: "Coming Soon...", WIP: "Coming Soon...",
Error: { Error: {
Unauthorized: Unauthorized:
"Unauthorized access, please enter access code in settings page.", "Unauthorized access, please enter access code in [auth](/#/auth) page.",
},
Auth: {
Title: "Need Access Code",
Tips: "Please enter access code below",
Input: "access code",
Confirm: "Confirm",
Later: "Later",
}, },
ChatItem: { ChatItem: {
ChatItemCount: (count: number) => `${count} messages`, ChatItemCount: (count: number) => `${count} messages`,
}, },
Chat: { Chat: {
SubTitle: (count: number) => `${count} messages with ChatGPT`, SubTitle: (count: number) => `${count} messages`,
Actions: { Actions: {
ChatList: "Go To Chat List", ChatList: "Go To Chat List",
CompressedHistory: "Compressed History Memory Prompt", CompressedHistory: "Compressed History Memory Prompt",
@@ -19,7 +26,32 @@ const en: RequiredLocaleType = {
Copy: "Copy", Copy: "Copy",
Stop: "Stop", Stop: "Stop",
Retry: "Retry", Retry: "Retry",
Pin: "Pin",
PinToastContent: "Pinned 2 messages to contextual prompts",
PinToastAction: "View",
Delete: "Delete", Delete: "Delete",
Edit: "Edit",
},
Commands: {
new: "Start a new chat",
newm: "Start a new chat with mask",
next: "Next Chat",
prev: "Previous Chat",
clear: "Clear Context",
del: "Delete Chat",
},
InputActions: {
Stop: "Stop",
ToBottom: "To Latest",
Theme: {
auto: "Auto",
light: "Light Theme",
dark: "Dark Theme",
},
Prompt: "Prompts",
Masks: "Masks",
Clear: "Clear Context",
Settings: "Settings",
}, },
Rename: "Rename Chat", Rename: "Rename Chat",
Typing: "Typing…", Typing: "Typing…",
@@ -28,7 +60,7 @@ const en: RequiredLocaleType = {
if (submitKey === String(SubmitKey.Enter)) { if (submitKey === String(SubmitKey.Enter)) {
inputHints += ", Shift + Enter to wrap"; inputHints += ", Shift + Enter to wrap";
} }
return inputHints + ", / to search prompts"; return inputHints + ", / to search prompts, : to use commands";
}, },
Send: "Send", Send: "Send",
Config: { Config: {
@@ -80,12 +112,19 @@ const en: RequiredLocaleType = {
Settings: { Settings: {
Title: "Settings", Title: "Settings",
SubTitle: "All Settings", SubTitle: "All Settings",
Actions: { Danger: {
ClearAll: "Clear All Data", Reset: {
ResetAll: "Reset All Settings", Title: "Reset All Settings",
Close: "Close", SubTitle: "Reset all setting items to default",
ConfirmResetAll: "Are you sure you want to reset all configurations?", Action: "Reset",
ConfirmClearAll: "Are you sure you want to reset all data?", Confirm: "Confirm to reset all settings to default?",
},
Clear: {
Title: "Clear All Data",
SubTitle: "Clear all messages and settings",
Action: "Clear",
Confirm: "Confirm to clear all messages and settings?",
},
}, },
Lang: { Lang: {
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language` Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
@@ -96,6 +135,12 @@ const en: RequiredLocaleType = {
Title: "Font Size", Title: "Font Size",
SubTitle: "Adjust font size of chat content", SubTitle: "Adjust font size of chat content",
}, },
InputTemplate: {
Title: "Input Template",
SubTitle: "Newest message will be filled to this template",
},
Update: { Update: {
Version: (x: string) => `Version: ${x}`, Version: (x: string) => `Version: ${x}`,
IsLatest: "Latest version", IsLatest: "Latest version",
@@ -161,6 +206,10 @@ const en: RequiredLocaleType = {
SubTitle: "Access control enabled", SubTitle: "Access control enabled",
Placeholder: "Need Access Code", Placeholder: "Need Access Code",
}, },
Endpoint: {
Title: "Endpoint",
SubTitle: "Custom endpoint must start with http(s)://",
},
Model: "Model", Model: "Model",
Temperature: { Temperature: {
Title: "Temperature", Title: "Temperature",
@@ -175,6 +224,11 @@ const en: RequiredLocaleType = {
SubTitle: SubTitle:
"A larger value increases the likelihood to talk about new topics", "A larger value increases the likelihood to talk about new topics",
}, },
FrequencyPenalty: {
Title: "Frequency Penalty",
SubTitle:
"A larger value decreasing the likelihood to repeat the same line",
},
}, },
Store: { Store: {
DefaultTopic: "New Conversation", DefaultTopic: "New Conversation",
@@ -256,6 +310,12 @@ const en: RequiredLocaleType = {
Create: "Create", Create: "Create",
Edit: "Edit", Edit: "Edit",
}, },
Exporter: {
Model: "Model",
Messages: "Messages",
Topic: "Topic",
Time: "Time",
},
}; };
export default en; export default en;

View File

@@ -1,7 +1,7 @@
import { SubmitKey } from "../store/config"; import { SubmitKey } from "../store/config";
import type { LocaleType } from "./index"; import type { PartialLocaleType } from "./index";
const es: LocaleType = { const es: PartialLocaleType = {
WIP: "En construcción...", WIP: "En construcción...",
Error: { Error: {
Unauthorized: Unauthorized:
@@ -61,13 +61,7 @@ const es: LocaleType = {
Settings: { Settings: {
Title: "Configuración", Title: "Configuración",
SubTitle: "Todas las configuraciones", SubTitle: "Todas las configuraciones",
Actions: {
ClearAll: "Borrar todos los datos",
ResetAll: "Restablecer todas las configuraciones",
Close: "Cerrar",
ConfirmResetAll: "Are you sure you want to reset all configurations?",
ConfirmClearAll: "Are you sure you want to reset all chat?",
},
Lang: { Lang: {
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language` Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
All: "Todos los idiomas", All: "Todos los idiomas",
@@ -156,6 +150,11 @@ const es: LocaleType = {
SubTitle: SubTitle:
"Un valor mayor aumenta la probabilidad de hablar sobre nuevos temas", "Un valor mayor aumenta la probabilidad de hablar sobre nuevos temas",
}, },
FrequencyPenalty: {
Title: "Penalización de frecuencia",
SubTitle:
"Un valor mayor que disminuye la probabilidad de repetir la misma línea",
},
}, },
Store: { Store: {
DefaultTopic: "Nueva conversación", DefaultTopic: "Nueva conversación",
@@ -228,6 +227,12 @@ const es: LocaleType = {
Create: "Create", Create: "Create",
Edit: "Edit", Edit: "Edit",
}, },
Exporter: {
Model: "Modelo",
Messages: "Mensajes",
Topic: "Tema",
Time: "Time",
},
}; };
export default es; export default es;

View File

@@ -1,7 +1,7 @@
import { SubmitKey } from "../store/config"; import { SubmitKey } from "../store/config";
import type { LocaleType } from "./index"; import type { PartialLocaleType } from "./index";
const fr: LocaleType = { const fr: PartialLocaleType = {
WIP: "Prochainement...", WIP: "Prochainement...",
Error: { Error: {
Unauthorized: Unauthorized:
@@ -61,14 +61,7 @@ const fr: LocaleType = {
Settings: { Settings: {
Title: "Paramètres", Title: "Paramètres",
SubTitle: "Toutes les configurations", SubTitle: "Toutes les configurations",
Actions: {
ClearAll: "Effacer toutes les données",
ResetAll: "Réinitialiser les configurations",
Close: "Fermer",
ConfirmResetAll:
"Êtes-vous sûr de vouloir réinitialiser toutes les configurations?",
ConfirmClearAll: "Êtes-vous sûr de vouloir supprimer toutes les données?",
},
Lang: { Lang: {
Name: "Language", // ATTENTION : si vous souhaitez ajouter une nouvelle traduction, ne traduisez pas cette valeur, laissez-la sous forme de `Language` Name: "Language", // ATTENTION : si vous souhaitez ajouter une nouvelle traduction, ne traduisez pas cette valeur, laissez-la sous forme de `Language`
All: "Toutes les langues", All: "Toutes les langues",
@@ -159,6 +152,11 @@ const fr: LocaleType = {
SubTitle: SubTitle:
"Une valeur plus élevée augmentera la probabilité d'introduire de nouveaux sujets", "Une valeur plus élevée augmentera la probabilité d'introduire de nouveaux sujets",
}, },
FrequencyPenalty: {
Title: "Pénalité de fréquence",
SubTitle:
"Une valeur plus élevée diminuant la probabilité de répéter la même ligne",
},
}, },
Store: { Store: {
DefaultTopic: "Nouvelle conversation", DefaultTopic: "Nouvelle conversation",
@@ -232,6 +230,12 @@ const fr: LocaleType = {
Create: "Créer", Create: "Créer",
Edit: "Éditer", Edit: "Éditer",
}, },
Exporter: {
Model: "Modèle",
Messages: "Messages",
Topic: "Sujet",
Time: "Temps",
},
}; };
export default fr; export default fr;

View File

@@ -1,56 +1,74 @@
import CN from "./cn"; import cn from "./cn";
import EN from "./en"; import en from "./en";
import TW from "./tw"; import tw from "./tw";
import FR from "./fr"; import fr from "./fr";
import ES from "./es"; import es from "./es";
import IT from "./it"; import it from "./it";
import TR from "./tr"; import tr from "./tr";
import JP from "./jp"; import jp from "./jp";
import DE from "./de"; import de from "./de";
import VI from "./vi"; import vi from "./vi";
import RU from "./ru"; import ru from "./ru";
import CS from "./cs"; import no from "./no";
import KO from "./ko"; import cs from "./cs";
import ko from "./ko";
import ar from "./ar";
import { merge } from "../utils/merge"; import { merge } from "../utils/merge";
export type { LocaleType, RequiredLocaleType } from "./cn"; import type { LocaleType } from "./cn";
export type { LocaleType, PartialLocaleType } from "./cn";
export const AllLangs = [ const ALL_LANGS = {
"en", cn,
"cn", en,
"tw", tw,
"fr", jp,
"es", ko,
"it", fr,
"tr", es,
"jp", it,
"de", tr,
"vi", de,
"ru", vi,
"cs", ru,
"ko", cs,
] as const; no,
export type Lang = (typeof AllLangs)[number]; ar,
};
export type Lang = keyof typeof ALL_LANGS;
export const AllLangs = Object.keys(ALL_LANGS) as Lang[];
export const ALL_LANG_OPTIONS: Record<Lang, string> = { export const ALL_LANG_OPTIONS: Record<Lang, string> = {
cn: "简体中文", cn: "简体中文",
en: "English", en: "English",
tw: "繁體中文", tw: "繁體中文",
jp: "日本語",
ko: "한국어",
fr: "Français", fr: "Français",
es: "Español", es: "Español",
it: "Italiano", it: "Italiano",
tr: "Türkçe", tr: "Türkçe",
jp: "日本語",
de: "Deutsch", de: "Deutsch",
vi: "Tiếng Việt", vi: "Tiếng Việt",
ru: "Русский", ru: "Русский",
cs: "Čeština", cs: "Čeština",
ko: "한국어", no: "Nynorsk",
ar: "العربية",
}; };
const LANG_KEY = "lang"; const LANG_KEY = "lang";
const DEFAULT_LANG = "en"; const DEFAULT_LANG = "en";
const fallbackLang = en;
const targetLang = ALL_LANGS[getLang()] as LocaleType;
// if target lang missing some fields, it will use fallback lang string
merge(fallbackLang, targetLang);
export default fallbackLang as LocaleType;
function getItem(key: string) { function getItem(key: string) {
try { try {
return localStorage.getItem(key); return localStorage.getItem(key);
@@ -69,7 +87,6 @@ function getLanguage() {
try { try {
return navigator.language.toLowerCase(); return navigator.language.toLowerCase();
} catch { } catch {
console.log("[Lang] failed to detect user lang.");
return DEFAULT_LANG; return DEFAULT_LANG;
} }
} }
@@ -96,25 +113,3 @@ export function changeLang(lang: Lang) {
setItem(LANG_KEY, lang); setItem(LANG_KEY, lang);
location.reload(); location.reload();
} }
const fallbackLang = EN;
const targetLang = {
en: EN,
cn: CN,
tw: TW,
fr: FR,
es: ES,
it: IT,
tr: TR,
jp: JP,
de: DE,
vi: VI,
ru: RU,
cs: CS,
ko: KO,
}[getLang()] as typeof CN;
// if target lang missing some fields, it will use fallback lang string
merge(fallbackLang, targetLang);
export default fallbackLang as typeof CN;

View File

@@ -1,7 +1,7 @@
import { SubmitKey } from "../store/config"; import { SubmitKey } from "../store/config";
import type { LocaleType } from "./index"; import type { PartialLocaleType } from "./index";
const it: LocaleType = { const it: PartialLocaleType = {
WIP: "Work in progress...", WIP: "Work in progress...",
Error: { Error: {
Unauthorized: Unauthorized:
@@ -61,13 +61,7 @@ const it: LocaleType = {
Settings: { Settings: {
Title: "Impostazioni", Title: "Impostazioni",
SubTitle: "Tutte le impostazioni", SubTitle: "Tutte le impostazioni",
Actions: {
ClearAll: "Cancella tutti i dati",
ResetAll: "Resetta tutte le impostazioni",
Close: "Chiudi",
ConfirmResetAll: "Sei sicuro vuoi cancellare tutte le impostazioni?",
ConfirmClearAll: "Sei sicuro vuoi cancellare tutte le chat?",
},
Lang: { Lang: {
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language` Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
All: "Tutte le lingue", All: "Tutte le lingue",
@@ -157,6 +151,11 @@ const it: LocaleType = {
SubTitle: SubTitle:
"Un valore maggiore aumenta la probabilità di parlare di nuovi argomenti", "Un valore maggiore aumenta la probabilità di parlare di nuovi argomenti",
}, },
FrequencyPenalty: {
Title: "Penalità di frequenza",
SubTitle:
"Un valore maggiore che diminuisce la probabilità di ripetere la stessa riga",
},
}, },
Store: { Store: {
DefaultTopic: "Nuova conversazione", DefaultTopic: "Nuova conversazione",
@@ -229,6 +228,12 @@ const it: LocaleType = {
Create: "Create", Create: "Create",
Edit: "Edit", Edit: "Edit",
}, },
Exporter: {
Model: "Modello",
Messages: "Messaggi",
Topic: "Argomento",
Time: "Tempo",
},
}; };
export default it; export default it;

View File

@@ -1,11 +1,11 @@
import { SubmitKey } from "../store/config"; import { SubmitKey } from "../store/config";
import type { LocaleType } from "./index"; import type { PartialLocaleType } from "./index";
const jp: LocaleType = { const jp: PartialLocaleType = {
WIP: "この機能は開発中です……", WIP: "この機能は開発中です",
Error: { Error: {
Unauthorized: Unauthorized:
"現在は未承認状態です。左下の設定ボタンをクリックし、アクセスパスワードを入力してください。", "現在は未承認状態です。左下の設定ボタンをクリックし、アクセスパスワードかOpenAIのAPIキーを入力してください。",
}, },
ChatItem: { ChatItem: {
ChatItemCount: (count: number) => `${count} 通のチャット`, ChatItemCount: (count: number) => `${count} 通のチャット`,
@@ -19,7 +19,7 @@ const jp: LocaleType = {
Copy: "コピー", Copy: "コピー",
Stop: "停止", Stop: "停止",
Retry: "リトライ", Retry: "リトライ",
Delete: "Delete", Delete: "削除",
}, },
Rename: "チャットの名前を変更", Rename: "チャットの名前を変更",
Typing: "入力中…", Typing: "入力中…",
@@ -32,7 +32,7 @@ const jp: LocaleType = {
}, },
Send: "送信", Send: "送信",
Config: { Config: {
Reset: "重置默认", Reset: "リセット",
SaveAs: "另存为面具", SaveAs: "另存为面具",
}, },
}, },
@@ -61,16 +61,10 @@ const jp: LocaleType = {
Settings: { Settings: {
Title: "設定", Title: "設定",
SubTitle: "設定オプション", SubTitle: "設定オプション",
Actions: {
ClearAll: "すべてのデータをクリア",
ResetAll: "すべてのオプションをリセット",
Close: "閉じる",
ConfirmResetAll: "すべての設定をリセットしてもよろしいですか?",
ConfirmClearAll: "すべてのチャットをリセットしてもよろしいですか?",
},
Lang: { Lang: {
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language` Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
All: "所有语言", All: "全ての言語",
}, },
Avatar: "アバター", Avatar: "アバター",
FontSize: { FontSize: {
@@ -91,11 +85,11 @@ const jp: LocaleType = {
TightBorder: "ボーダーレスモード", TightBorder: "ボーダーレスモード",
SendPreviewBubble: { SendPreviewBubble: {
Title: "プレビューバブルの送信", Title: "プレビューバブルの送信",
SubTitle: "在预览气泡中预览 Markdown 内容", SubTitle: "プレビューバブルでマークダウンコンテンツをプレビュー",
}, },
Mask: { Mask: {
Title: "面具启动页", Title: "キャラクターページ",
SubTitle: "新建聊天时,展示面具启动页", SubTitle: "新規チャット作成時にキャラクターページを表示する",
}, },
Prompt: { Prompt: {
Disable: { Disable: {
@@ -113,7 +107,7 @@ const jp: LocaleType = {
Search: "プロンプトワード検索", Search: "プロンプトワード検索",
}, },
EditModal: { EditModal: {
Title: "编辑提示词", Title: "編集",
}, },
}, },
HistoryCount: { HistoryCount: {
@@ -158,6 +152,10 @@ const jp: LocaleType = {
Title: "トピックの新鮮度 (presence_penalty)", Title: "トピックの新鮮度 (presence_penalty)",
SubTitle: "値が大きいほど、新しいトピックへの展開が可能になります。", SubTitle: "値が大きいほど、新しいトピックへの展開が可能になります。",
}, },
FrequencyPenalty: {
Title: "話題の頻度 (frequency_penalty)",
SubTitle: "値が大きいほど、重複語を低減する可能性が高くなります",
},
}, },
Store: { Store: {
DefaultTopic: "新しいチャット", DefaultTopic: "新しいチャット",
@@ -178,54 +176,70 @@ const jp: LocaleType = {
Failed: "コピーに失敗しました。クリップボード許可を与えてください。", Failed: "コピーに失敗しました。クリップボード許可を与えてください。",
}, },
Context: { Context: {
Toast: (x: any) => `前置コンテキスト${x} 件設定されました`, Toast: (x: any) => `キャラクター${x} 件設定されました`,
Edit: "前置コンテキストと履歴メモリ", Edit: "キャラクタープリセットとモデル設定",
Add: "新規追加", Add: "追加",
}, },
Plugin: { Name: "插件" }, Plugin: { Name: "プラグイン" },
Mask: { Mask: {
Name: "面具", Name: "キャラクタープリセット",
Page: { Page: {
Title: "预设角色面具", Title: "キャラクタープリセット",
SubTitle: (count: number) => `${count} 个预设角色定义`, SubTitle: (count: number) => `${count} 件見つかりました。`,
Search: "搜索角色面具", Search: "検索",
Create: "新", Create: "新",
}, },
Item: { Item: {
Info: (count: number) => `包含 ${count} 条预设对话`, Info: (count: number) => `包含 ${count} 条预设对话`,
Chat: "对话", Chat: "会話",
View: "查看", View: "詳細",
Edit: "编辑", Edit: "編集",
Delete: "除", Delete: "除",
DeleteConfirm: "确认删除", DeleteConfirm: "本当に削除しますか",
}, },
EditModal: { EditModal: {
Title: (readonly: boolean) => Title: (readonly: boolean) =>
`编辑预设面具 ${readonly ? "只读" : ""}`, `キャラクタープリセットを編集 ${readonly ? "読み取り専用" : ""}`,
Download: "下载预设", Download: "ダウンロード",
Clone: "克隆预设", Clone: "複製",
}, },
Config: { Config: {
Avatar: "角色头像", Avatar: "キャラクターのアイコン",
Name: "角色名称", Name: "キャラクターの名前",
Sync: {
Title: "グローバル設定を利用する",
SubTitle: "このチャットでグローバル設定を利用します。",
Confirm:
"カスタム設定を上書きしてグローバル設定を使用します、よろしいですか?",
},
HideContext: {
Title: "キャラクター設定を表示しない",
SubTitle: "チャット画面でのキャラクター設定を非表示にします。",
},
}, },
}, },
NewChat: { NewChat: {
Return: "返回", Return: "戻る",
Skip: "跳过", Skip: "スキップ",
Title: "挑选一个面具", Title: "キャラクター",
SubTitle: "现在开始,与面具背后的灵魂思维碰撞", SubTitle: "さあ、AIにキャラクターを設定して会話を始めてみましょう",
More: "搜索更多", More: "もっと探す",
NotShow: "不再展示", NotShow: "今後は表示しない",
ConfirmNoShow: "确认禁用?禁用后可以随时在设置中重新启用。", ConfirmNoShow: "いつでも設定から有効化できます。",
}, },
UI: { UI: {
Confirm: "确认", Confirm: "確認",
Cancel: "取消", Cancel: "キャンセル",
Close: "关闭", Close: "閉じる",
Create: "新", Create: "新",
Edit: "编辑", Edit: "編集",
},
Exporter: {
Model: "モデル",
Messages: "メッセージ",
Topic: "トピック",
Time: "時間",
}, },
}; };

View File

@@ -1,8 +1,8 @@
import { SubmitKey } from "../store/config"; import { SubmitKey } from "../store/config";
import type { LocaleType } from "./index"; import type { PartialLocaleType } from "./index";
const ko: LocaleType = { const ko: PartialLocaleType = {
WIP: "곧 출시 예정...", WIP: "곧 출시 예정...",
Error: { Error: {
Unauthorized: "권한이 없습니다. 설정 페이지에서 액세스 코드를 입력하세요.", Unauthorized: "권한이 없습니다. 설정 페이지에서 액세스 코드를 입력하세요.",
@@ -61,13 +61,7 @@ const ko: LocaleType = {
Settings: { Settings: {
Title: "설정", Title: "설정",
SubTitle: "모든 설정", SubTitle: "모든 설정",
Actions: {
ClearAll: "모든 데이터 지우기",
ResetAll: "모든 설정 초기화",
Close: "닫기",
ConfirmResetAll: "모든 설정을 초기화하시겠습니까?",
ConfirmClearAll: "모든 데이터를 지우시겠습니까?",
},
Lang: { Lang: {
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language` Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
All: "All Languages", All: "All Languages",
@@ -154,6 +148,10 @@ const ko: LocaleType = {
Title: "존재 페널티 (presence_penalty)", Title: "존재 페널티 (presence_penalty)",
SubTitle: "값이 클수록 새로운 주제에 대해 대화할 가능성이 높아집니다.", SubTitle: "값이 클수록 새로운 주제에 대해 대화할 가능성이 높아집니다.",
}, },
FrequencyPenalty: {
Title: "빈도 페널티(frequency penalty)",
SubTitle: "값이 클수록 같은 줄이 반복될 가능성이 줄어듭니다.",
},
}, },
Store: { Store: {
DefaultTopic: "새 대화", DefaultTopic: "새 대화",
@@ -225,6 +223,12 @@ const ko: LocaleType = {
Create: "생성", Create: "생성",
Edit: "편집", Edit: "편집",
}, },
Exporter: {
Model: "모델",
Messages: "메시지",
Topic: "주제",
Time: "시간",
},
}; };
export default ko; export default ko;

164
app/locales/no.ts Normal file
View File

@@ -0,0 +1,164 @@
import { SubmitKey } from "../store/config";
import type { PartialLocaleType } from "./index";
const no: PartialLocaleType = {
WIP: "Arbeid pågår ...",
Error: {
Unauthorized: "Du har ikke tilgang. Vennlig oppgi tildelt adgangskode.",
},
ChatItem: {
ChatItemCount: (count: number) => `${count} meldinger`,
},
Chat: {
SubTitle: (count: number) => `${count} meldinger med ChatGPT`,
Actions: {
ChatList: "Gå til chatlisten",
CompressedHistory: "Komprimert historikk for instrukser",
Export: "Eksporter alle meldinger i markdown-format",
Copy: "Kopier",
Stop: "Stopp",
Retry: "Prøv igjen",
Delete: "Slett",
},
Rename: "Gi nytt navn",
Typing: "Skriver …",
Input: (submitKey: string) => {
var inputHints = `${submitKey} for å sende`;
if (submitKey === String(SubmitKey.Enter)) {
inputHints += ", Shift + Enter for å omgi";
}
return inputHints + ", / for å søke instrukser";
},
Send: "Send",
},
Export: {
Title: "Alle meldinger",
Copy: "Kopiere alle",
Download: "Last ned",
MessageFromYou: "Melding fra deg",
MessageFromChatGPT: "Melding fra ChatGPT",
},
Memory: {
Title: "Minneinstruks",
EmptyContent: "Ingen sålant.",
Send: "Send minne",
Copy: "Kopiere minne",
Reset: "Nulstill sesjon",
ResetConfirm:
"Om du nillstiller vil du slette hele historikken. Er du sikker på at du vil nullstille?",
},
Home: {
NewChat: "Ny chat",
DeleteChat: "Bekreft for å slette det valgte dialogen",
DeleteToast: "Samtale slettet",
Revert: "Tilbakestill",
},
Settings: {
Title: "Innstillinger",
SubTitle: "Alle innstillinger",
Lang: {
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
},
Avatar: "Avatar",
FontSize: {
Title: "Fontstørrelsen",
SubTitle: "Juster fontstørrelsen for samtaleinnholdet.",
},
Update: {
Version: (x: string) => `Versjon: ${x}`,
IsLatest: "Siste versjon",
CheckUpdate: "Se etter oppdatering",
IsChecking: "Ser etter oppdatering ...",
FoundUpdate: (x: string) => `Fant ny versjon: ${x}`,
GoToUpdate: "Oppdater",
},
SendKey: "Send nøkkel",
Theme: "Tema",
TightBorder: "Stram innramming",
Prompt: {
Disable: {
Title: "Skru av autofullfør",
SubTitle: "Skriv / for å trigge autofullfør",
},
List: "Instruksliste",
ListCount: (builtin: number, custom: number) =>
`${builtin} innebygde, ${custom} brukerdefinerte`,
Edit: "Endre",
Modal: {
Title: "Instruksliste",
Add: "Legg til",
Search: "Søk instrukser",
},
},
HistoryCount: {
Title: "Tall på tilhørende meldinger",
SubTitle: "Antall sendte meldinger tilknyttet hver spørring",
},
CompressThreshold: {
Title: "Terskeverdi for komprimering av historikk",
SubTitle:
"Komprimer dersom ikke-komprimert lengde på meldinger overskrider denne verdien",
},
Token: {
Title: "API Key",
SubTitle:
"Bruk din egen API-nøkkel for å ignorere tilgangskoden begrensning",
Placeholder: "OpenAI API-nøkkel",
},
Usage: {
Title: "Saldo for konto",
SubTitle(used: any, total: any) {
return `Brukt denne måneden $${used}, abonnement $${total}`;
},
IsChecking: "Sjekker ...",
Check: "Sjekk",
NoAccess: "Skriv inn API-nøkkelen for å sjekke saldo",
},
AccessCode: {
Title: "Tilgangskode",
SubTitle: "Tilgangskontroll på",
Placeholder: "Trenger tilgangskode",
},
Model: "Model",
Temperature: {
Title: "Temperatur",
SubTitle: "Høyere verdi gir mer kreative svar",
},
MaxTokens: {
Title: "Maks tokens",
SubTitle: "Maksimum lengde på tokens for instrukser og svar",
},
},
Store: {
DefaultTopic: "Ny samtale",
BotHello: "Hei! Hva kan jeg hjelpe deg med i dag?",
Error: "Noe gikk galt, vennligst prøv igjen senere.",
Prompt: {
History: (content: string) =>
"Dette er et sammendrag av chatthistorikken mellom AI-en og brukeren som en oppsummering: " +
content,
Topic:
"Vennligst lag en fire til fem ords tittel som oppsummerer samtalen vår uten innledning, punktsetting, anførselstegn, punktum, symboler eller tillegg tekst. Fjern innrammende anførselstegn.",
Summarize:
"Oppsummer diskusjonen vår kort i 200 ord eller mindre for å bruke som en oppfordring til fremtidig sammenheng.",
},
},
Copy: {
Success: "Kopiert til utklippstavle",
Failed: "Kopiering feilet. Vennligst gi tilgang til utklippstavlen.",
},
Context: {
Toast: (x: any) => `Med ${x} kontekstuelle instrukser`,
Edit: "Kontekstuelle -og minneinstrukser",
Add: "Legg til",
},
Exporter: {
Model: "Model",
Messages: "Meldingar",
Topic: "Emne",
Time: "Tid",
},
};
export default no;

View File

@@ -1,7 +1,7 @@
import { SubmitKey } from "../store/config"; import { SubmitKey } from "../store/config";
import type { LocaleType } from "./index"; import type { PartialLocaleType } from "./index";
const ru: LocaleType = { const ru: PartialLocaleType = {
WIP: "Скоро...", WIP: "Скоро...",
Error: { Error: {
Unauthorized: Unauthorized:
@@ -61,13 +61,7 @@ const ru: LocaleType = {
Settings: { Settings: {
Title: "Настройки", Title: "Настройки",
SubTitle: "Все настройки", SubTitle: "Все настройки",
Actions: {
ClearAll: "Очистить все данные",
ResetAll: "Сбросить все настройки",
Close: "Закрыть",
ConfirmResetAll: "Вы уверены, что хотите сбросить все настройки?",
ConfirmClearAll: "Вы уверены, что хотите очистить все данные?",
},
Lang: { Lang: {
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language` Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
All: "Все языки", All: "Все языки",
@@ -157,6 +151,11 @@ const ru: LocaleType = {
SubTitle: SubTitle:
"Чем выше значение, тем больше вероятность общения на новые темы", "Чем выше значение, тем больше вероятность общения на новые темы",
}, },
FrequencyPenalty: {
Title: "Штраф за частоту",
SubTitle:
"Большее значение снижает вероятность повторения одной и той же строки",
},
}, },
Store: { Store: {
DefaultTopic: "Новый разговор", DefaultTopic: "Новый разговор",
@@ -232,6 +231,12 @@ const ru: LocaleType = {
Create: "Создать", Create: "Создать",
Edit: "Редактировать", Edit: "Редактировать",
}, },
Exporter: {
Model: "Модель",
Messages: "Сообщения",
Topic: "Тема",
Time: "Время",
},
}; };
export default ru; export default ru;

View File

@@ -1,7 +1,7 @@
import { SubmitKey } from "../store/config"; import { SubmitKey } from "../store/config";
import type { LocaleType } from "./index"; import type { PartialLocaleType } from "./index";
const tr: LocaleType = { const tr: PartialLocaleType = {
WIP: "Çalışma devam ediyor...", WIP: "Çalışma devam ediyor...",
Error: { Error: {
Unauthorized: Unauthorized:
@@ -61,13 +61,7 @@ const tr: LocaleType = {
Settings: { Settings: {
Title: "Ayarlar", Title: "Ayarlar",
SubTitle: "Tüm Ayarlar", SubTitle: "Tüm Ayarlar",
Actions: {
ClearAll: "Tüm Verileri Temizle",
ResetAll: "Tüm Ayarları Sıfırla",
Close: "Kapat",
ConfirmResetAll: "Tüm ayarları sıfırlamak istediğinizden emin misiniz?",
ConfirmClearAll: "Tüm sohbeti sıfırlamak istediğinizden emin misiniz?",
},
Lang: { Lang: {
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language` Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
All: "Tüm Diller", All: "Tüm Diller",
@@ -158,6 +152,11 @@ const tr: LocaleType = {
SubTitle: SubTitle:
"Daha büyük bir değer, yeni konular hakkında konuşma olasılığını artırır", "Daha büyük bir değer, yeni konular hakkında konuşma olasılığını artırır",
}, },
FrequencyPenalty: {
Title: "Frekans Cezası",
SubTitle:
"Aynı satırı tekrar etme olasılığını azaltan daha büyük bir değer",
},
}, },
Store: { Store: {
DefaultTopic: "Yeni Konuşma", DefaultTopic: "Yeni Konuşma",
@@ -229,6 +228,12 @@ const tr: LocaleType = {
Create: "Create", Create: "Create",
Edit: "Edit", Edit: "Edit",
}, },
Exporter: {
Model: "Model",
Messages: "Mesajlar",
Topic: "Konu",
Time: "Zaman",
},
}; };
export default tr; export default tr;

View File

@@ -1,7 +1,7 @@
import { SubmitKey } from "../store/config"; import { SubmitKey } from "../store/config";
import type { LocaleType } from "./index"; import type { PartialLocaleType } from "./index";
const tw: LocaleType = { const tw: PartialLocaleType = {
WIP: "該功能仍在開發中……", WIP: "該功能仍在開發中……",
Error: { Error: {
Unauthorized: "目前您的狀態是未授權,請前往設定頁面輸入授權碼。", Unauthorized: "目前您的狀態是未授權,請前往設定頁面輸入授權碼。",
@@ -59,13 +59,7 @@ const tw: LocaleType = {
Settings: { Settings: {
Title: "設定", Title: "設定",
SubTitle: "設定選項", SubTitle: "設定選項",
Actions: {
ClearAll: "清除所有資料",
ResetAll: "重設所有設定",
Close: "關閉",
ConfirmResetAll: "您確定要重設所有設定嗎?",
ConfirmClearAll: "您確定要清除所有数据嗎?",
},
Lang: { Lang: {
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language` Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
All: "所有语言", All: "所有语言",
@@ -152,6 +146,10 @@ const tw: LocaleType = {
Title: "話題新穎度 (presence_penalty)", Title: "話題新穎度 (presence_penalty)",
SubTitle: "值越大,越有可能擴展到新話題", SubTitle: "值越大,越有可能擴展到新話題",
}, },
FrequencyPenalty: {
Title: "頻率懲罰度 (frequency_penalty)",
SubTitle: "值越大,越有可能降低重複字詞",
},
}, },
Store: { Store: {
DefaultTopic: "新的對話", DefaultTopic: "新的對話",
@@ -219,6 +217,12 @@ const tw: LocaleType = {
Create: "新建", Create: "新建",
Edit: "编辑", Edit: "编辑",
}, },
Exporter: {
Model: "模型",
Messages: "消息",
Topic: "主題",
Time: "時間",
},
}; };
export default tw; export default tw;

View File

@@ -1,7 +1,7 @@
import { SubmitKey } from "../store/config"; import { SubmitKey } from "../store/config";
import type { LocaleType } from "./index"; import type { PartialLocaleType } from "./index";
const vi: LocaleType = { const vi: PartialLocaleType = {
WIP: "Sắp ra mắt...", WIP: "Sắp ra mắt...",
Error: { Error: {
Unauthorized: Unauthorized:
@@ -61,13 +61,7 @@ const vi: LocaleType = {
Settings: { Settings: {
Title: "Cài đặt", Title: "Cài đặt",
SubTitle: "Tất cả cài đặt", SubTitle: "Tất cả cài đặt",
Actions: {
ClearAll: "Xóa toàn bộ dữ liệu",
ResetAll: "Khôi phục cài đặt gốc",
Close: "Đóng",
ConfirmResetAll: "Bạn chắc chắn muốn thiết lập lại tất cả cài đặt?",
ConfirmClearAll: "Bạn chắc chắn muốn thiết lập lại tất cả dữ liệu?",
},
Lang: { Lang: {
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language` Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
All: "Tất cả ngôn ngữ", All: "Tất cả ngôn ngữ",
@@ -154,6 +148,10 @@ const vi: LocaleType = {
Title: "Chủ đề mới (presence_penalty)", Title: "Chủ đề mới (presence_penalty)",
SubTitle: "Giá trị càng lớn tăng khả năng mở rộng sang các chủ đề mới", SubTitle: "Giá trị càng lớn tăng khả năng mở rộng sang các chủ đề mới",
}, },
FrequencyPenalty: {
Title: "Hình phạt tần suất",
SubTitle: "Giá trị lớn hơn làm giảm khả năng lặp lại cùng một dòng",
},
}, },
Store: { Store: {
DefaultTopic: "Cuộc trò chuyện mới", DefaultTopic: "Cuộc trò chuyện mới",
@@ -225,6 +223,12 @@ const vi: LocaleType = {
Create: "Tạo", Create: "Tạo",
Edit: "Chỉnh sửa", Edit: "Chỉnh sửa",
}, },
Exporter: {
Model: "Mô hình",
Messages: "Thông điệp",
Topic: "Chủ đề",
Time: "Thời gian",
},
}; };
export default vi; export default vi;

View File

@@ -29,6 +29,7 @@ export const CN_MASKS: BuiltinMask[] = [
temperature: 1, temperature: 1,
max_tokens: 2000, max_tokens: 2000,
presence_penalty: 0, presence_penalty: 0,
frequency_penalty: 0,
sendMemory: true, sendMemory: true,
historyMessageCount: 32, historyMessageCount: 32,
compressMessageLengthThreshold: 1000, compressMessageLengthThreshold: 1000,
@@ -52,6 +53,7 @@ export const CN_MASKS: BuiltinMask[] = [
temperature: 1, temperature: 1,
max_tokens: 2000, max_tokens: 2000,
presence_penalty: 0, presence_penalty: 0,
frequency_penalty: 0,
sendMemory: true, sendMemory: true,
historyMessageCount: 4, historyMessageCount: 4,
compressMessageLengthThreshold: 1000, compressMessageLengthThreshold: 1000,
@@ -75,6 +77,7 @@ export const CN_MASKS: BuiltinMask[] = [
temperature: 1, temperature: 1,
max_tokens: 2000, max_tokens: 2000,
presence_penalty: 0, presence_penalty: 0,
frequency_penalty: 0,
sendMemory: true, sendMemory: true,
historyMessageCount: 4, historyMessageCount: 4,
compressMessageLengthThreshold: 1000, compressMessageLengthThreshold: 1000,
@@ -98,6 +101,7 @@ export const CN_MASKS: BuiltinMask[] = [
temperature: 1, temperature: 1,
max_tokens: 2000, max_tokens: 2000,
presence_penalty: 0, presence_penalty: 0,
frequency_penalty: 0,
sendMemory: true, sendMemory: true,
historyMessageCount: 4, historyMessageCount: 4,
compressMessageLengthThreshold: 1000, compressMessageLengthThreshold: 1000,
@@ -121,6 +125,7 @@ export const CN_MASKS: BuiltinMask[] = [
temperature: 1, temperature: 1,
max_tokens: 2000, max_tokens: 2000,
presence_penalty: 0, presence_penalty: 0,
frequency_penalty: 0,
sendMemory: true, sendMemory: true,
historyMessageCount: 4, historyMessageCount: 4,
compressMessageLengthThreshold: 1000, compressMessageLengthThreshold: 1000,
@@ -144,6 +149,7 @@ export const CN_MASKS: BuiltinMask[] = [
temperature: 1, temperature: 1,
max_tokens: 2000, max_tokens: 2000,
presence_penalty: 0, presence_penalty: 0,
frequency_penalty: 0,
sendMemory: false, sendMemory: false,
historyMessageCount: 4, historyMessageCount: 4,
compressMessageLengthThreshold: 1000, compressMessageLengthThreshold: 1000,
@@ -167,6 +173,7 @@ export const CN_MASKS: BuiltinMask[] = [
temperature: 1, temperature: 1,
max_tokens: 2000, max_tokens: 2000,
presence_penalty: 0, presence_penalty: 0,
frequency_penalty: 0,
sendMemory: false, sendMemory: false,
historyMessageCount: 4, historyMessageCount: 4,
compressMessageLengthThreshold: 1000, compressMessageLengthThreshold: 1000,
@@ -190,6 +197,7 @@ export const CN_MASKS: BuiltinMask[] = [
temperature: 1, temperature: 1,
max_tokens: 2000, max_tokens: 2000,
presence_penalty: 0, presence_penalty: 0,
frequency_penalty: 0,
sendMemory: false, sendMemory: false,
historyMessageCount: 0, historyMessageCount: 0,
compressMessageLengthThreshold: 1000, compressMessageLengthThreshold: 1000,
@@ -218,6 +226,7 @@ export const CN_MASKS: BuiltinMask[] = [
temperature: 0.5, temperature: 0.5,
max_tokens: 2000, max_tokens: 2000,
presence_penalty: 0, presence_penalty: 0,
frequency_penalty: 0,
sendMemory: true, sendMemory: true,
historyMessageCount: 4, historyMessageCount: 4,
compressMessageLengthThreshold: 1000, compressMessageLengthThreshold: 1000,
@@ -241,6 +250,7 @@ export const CN_MASKS: BuiltinMask[] = [
temperature: 1, temperature: 1,
max_tokens: 2000, max_tokens: 2000,
presence_penalty: 0, presence_penalty: 0,
frequency_penalty: 0,
sendMemory: true, sendMemory: true,
historyMessageCount: 4, historyMessageCount: 4,
compressMessageLengthThreshold: 1000, compressMessageLengthThreshold: 1000,
@@ -270,6 +280,7 @@ export const CN_MASKS: BuiltinMask[] = [
temperature: 1, temperature: 1,
max_tokens: 2000, max_tokens: 2000,
presence_penalty: 0, presence_penalty: 0,
frequency_penalty: 0,
sendMemory: false, sendMemory: false,
historyMessageCount: 4, historyMessageCount: 4,
compressMessageLengthThreshold: 1000, compressMessageLengthThreshold: 1000,
@@ -299,6 +310,7 @@ export const CN_MASKS: BuiltinMask[] = [
temperature: 1, temperature: 1,
max_tokens: 2000, max_tokens: 2000,
presence_penalty: 0, presence_penalty: 0,
frequency_penalty: 0,
sendMemory: false, sendMemory: false,
historyMessageCount: 4, historyMessageCount: 4,
compressMessageLengthThreshold: 1000, compressMessageLengthThreshold: 1000,
@@ -349,6 +361,7 @@ export const CN_MASKS: BuiltinMask[] = [
temperature: 1, temperature: 1,
max_tokens: 2000, max_tokens: 2000,
presence_penalty: 0, presence_penalty: 0,
frequency_penalty: 0,
sendMemory: false, sendMemory: false,
historyMessageCount: 4, historyMessageCount: 4,
compressMessageLengthThreshold: 1000, compressMessageLengthThreshold: 1000,
@@ -377,6 +390,7 @@ export const CN_MASKS: BuiltinMask[] = [
temperature: 0.5, temperature: 0.5,
max_tokens: 2000, max_tokens: 2000,
presence_penalty: 0, presence_penalty: 0,
frequency_penalty: 0,
sendMemory: true, sendMemory: true,
historyMessageCount: 4, historyMessageCount: 4,
compressMessageLengthThreshold: 1000, compressMessageLengthThreshold: 1000,

View File

@@ -17,6 +17,7 @@ export const EN_MASKS: BuiltinMask[] = [
temperature: 0.3, temperature: 0.3,
max_tokens: 2000, max_tokens: 2000,
presence_penalty: 0, presence_penalty: 0,
frequency_penalty: 0,
sendMemory: true, sendMemory: true,
historyMessageCount: 4, historyMessageCount: 4,
compressMessageLengthThreshold: 1000, compressMessageLengthThreshold: 1000,
@@ -57,6 +58,7 @@ export const EN_MASKS: BuiltinMask[] = [
temperature: 0.5, temperature: 0.5,
max_tokens: 2000, max_tokens: 2000,
presence_penalty: 0, presence_penalty: 0,
frequency_penalty: 0,
sendMemory: true, sendMemory: true,
historyMessageCount: 4, historyMessageCount: 4,
compressMessageLengthThreshold: 1000, compressMessageLengthThreshold: 1000,
@@ -80,6 +82,7 @@ export const EN_MASKS: BuiltinMask[] = [
temperature: 0.5, temperature: 0.5,
max_tokens: 2000, max_tokens: 2000,
presence_penalty: 0, presence_penalty: 0,
frequency_penalty: 0,
sendMemory: true, sendMemory: true,
historyMessageCount: 4, historyMessageCount: 4,
compressMessageLengthThreshold: 1000, compressMessageLengthThreshold: 1000,
@@ -108,6 +111,7 @@ export const EN_MASKS: BuiltinMask[] = [
temperature: 0.5, temperature: 0.5,
max_tokens: 2000, max_tokens: 2000,
presence_penalty: 0, presence_penalty: 0,
frequency_penalty: 0,
sendMemory: true, sendMemory: true,
historyMessageCount: 4, historyMessageCount: 4,
compressMessageLengthThreshold: 2000, compressMessageLengthThreshold: 2000,

View File

@@ -9,7 +9,7 @@ export const BUILTIN_MASK_ID = 100000;
export const BUILTIN_MASK_STORE = { export const BUILTIN_MASK_STORE = {
buildinId: BUILTIN_MASK_ID, buildinId: BUILTIN_MASK_ID,
masks: {} as Record<number, Mask>, masks: {} as Record<number, BuiltinMask>,
get(id?: number) { get(id?: number) {
if (!id) return undefined; if (!id) return undefined;
return this.masks[id] as Mask | undefined; return this.masks[id] as Mask | undefined;
@@ -21,6 +21,6 @@ export const BUILTIN_MASK_STORE = {
}, },
}; };
export const BUILTIN_MASKS: Mask[] = [...CN_MASKS, ...EN_MASKS].map((m) => export const BUILTIN_MASKS: BuiltinMask[] = [...CN_MASKS, ...EN_MASKS].map(
BUILTIN_MASK_STORE.add(m), (m) => BUILTIN_MASK_STORE.add(m),
); );

View File

@@ -1,5 +1,7 @@
import { ModelConfig } from "../store";
import { type Mask } from "../store/mask"; import { type Mask } from "../store/mask";
export type BuiltinMask = Omit<Mask, "id"> & { export type BuiltinMask = Omit<Mask, "id" | "modelConfig"> & {
builtin: true; builtin: Boolean;
modelConfig: Partial<ModelConfig>;
}; };

View File

@@ -1,9 +1,10 @@
import { create } from "zustand"; import { create } from "zustand";
import { persist } from "zustand/middleware"; import { persist } from "zustand/middleware";
import { StoreKey } from "../constant"; import { DEFAULT_API_HOST, StoreKey } from "../constant";
import { getHeaders } from "../client/api"; import { getHeaders } from "../client/api";
import { BOT_HELLO } from "./chat"; import { BOT_HELLO } from "./chat";
import { ALL_MODELS } from "./config"; import { ALL_MODELS } from "./config";
import { getClientConfig } from "../config/client";
export interface AccessControlStore { export interface AccessControlStore {
accessCode: string; accessCode: string;
@@ -12,9 +13,11 @@ export interface AccessControlStore {
needCode: boolean; needCode: boolean;
hideUserApiKey: boolean; hideUserApiKey: boolean;
openaiUrl: string; openaiUrl: string;
hideBalanceQuery: boolean;
updateToken: (_: string) => void; updateToken: (_: string) => void;
updateCode: (_: string) => void; updateCode: (_: string) => void;
updateOpenAiUrl: (_: string) => void;
enabledAccessControl: () => boolean; enabledAccessControl: () => boolean;
isAuthorized: () => boolean; isAuthorized: () => boolean;
fetch: () => void; fetch: () => void;
@@ -22,6 +25,10 @@ export interface AccessControlStore {
let fetchState = 0; // 0 not fetch, 1 fetching, 2 done let fetchState = 0; // 0 not fetch, 1 fetching, 2 done
const DEFAULT_OPENAI_URL =
getClientConfig()?.buildMode === "export" ? DEFAULT_API_HOST : "/api/openai/";
console.log("[API] default openai url", DEFAULT_OPENAI_URL);
export const useAccessStore = create<AccessControlStore>()( export const useAccessStore = create<AccessControlStore>()(
persist( persist(
(set, get) => ({ (set, get) => ({
@@ -29,7 +36,8 @@ export const useAccessStore = create<AccessControlStore>()(
accessCode: "", accessCode: "",
needCode: true, needCode: true,
hideUserApiKey: false, hideUserApiKey: false,
openaiUrl: "/api/openai/", openaiUrl: DEFAULT_OPENAI_URL,
hideBalanceQuery: false,
enabledAccessControl() { enabledAccessControl() {
get().fetch(); get().fetch();
@@ -42,6 +50,9 @@ export const useAccessStore = create<AccessControlStore>()(
updateToken(token: string) { updateToken(token: string) {
set(() => ({ token })); set(() => ({ token }));
}, },
updateOpenAiUrl(url: string) {
set(() => ({ openaiUrl: url }));
},
isAuthorized() { isAuthorized() {
get().fetch(); get().fetch();
@@ -51,7 +62,7 @@ export const useAccessStore = create<AccessControlStore>()(
); );
}, },
fetch() { fetch() {
if (fetchState > 0) return; if (fetchState > 0 || getClientConfig()?.buildMode === "export") return;
fetchState = 1; fetchState = 1;
fetch("/api/config", { fetch("/api/config", {
method: "post", method: "post",

View File

@@ -3,14 +3,19 @@ import { persist } from "zustand/middleware";
import { trimTopic } from "../utils"; import { trimTopic } from "../utils";
import Locale from "../locales"; import Locale, { getLang } from "../locales";
import { showToast } from "../components/ui-lib"; import { showToast } from "../components/ui-lib";
import { ModelType } from "./config"; import { ModelConfig, ModelType, useAppConfig } from "./config";
import { createEmptyMask, Mask } from "./mask"; import { createEmptyMask, Mask } from "./mask";
import { StoreKey } from "../constant"; import {
DEFAULT_INPUT_TEMPLATE,
DEFAULT_SYSTEM_TEMPLATE,
StoreKey,
} from "../constant";
import { api, RequestMessage } from "../client/api"; import { api, RequestMessage } from "../client/api";
import { ChatControllerPool } from "../client/controller"; import { ChatControllerPool } from "../client/controller";
import { prettyObject } from "../utils/format"; import { prettyObject } from "../utils/format";
import { estimateTokenLength } from "../utils/token";
export type ChatMessage = RequestMessage & { export type ChatMessage = RequestMessage & {
date: string; date: string;
@@ -84,6 +89,7 @@ interface ChatStore {
newSession: (mask?: Mask) => void; newSession: (mask?: Mask) => void;
deleteSession: (index: number) => void; deleteSession: (index: number) => void;
currentSession: () => ChatSession; currentSession: () => ChatSession;
nextSession: (delta: number) => void;
onNewMessage: (message: ChatMessage) => void; onNewMessage: (message: ChatMessage) => void;
onUserInput: (content: string) => Promise<void>; onUserInput: (content: string) => Promise<void>;
summarizeSession: () => void; summarizeSession: () => void;
@@ -102,7 +108,30 @@ interface ChatStore {
} }
function countMessages(msgs: ChatMessage[]) { function countMessages(msgs: ChatMessage[]) {
return msgs.reduce((pre, cur) => pre + cur.content.length, 0); return msgs.reduce((pre, cur) => pre + estimateTokenLength(cur.content), 0);
}
function fillTemplateWith(input: string, modelConfig: ModelConfig) {
const vars = {
model: modelConfig.model,
time: new Date().toLocaleString(),
lang: getLang(),
input: input,
};
let output = modelConfig.template ?? DEFAULT_INPUT_TEMPLATE;
// must contains {{input}}
const inputVar = "{{input}}";
if (!output.includes(inputVar)) {
output += "\n" + inputVar;
}
Object.entries(vars).forEach(([name, value]) => {
output = output.replaceAll(`{{${name}}}`, value);
});
return output;
} }
export const useChatStore = create<ChatStore>()( export const useChatStore = create<ChatStore>()(
@@ -157,7 +186,16 @@ export const useChatStore = create<ChatStore>()(
session.id = get().globalId; session.id = get().globalId;
if (mask) { if (mask) {
session.mask = { ...mask }; const config = useAppConfig.getState();
const globalModelConfig = config.modelConfig;
session.mask = {
...mask,
modelConfig: {
...globalModelConfig,
...mask.modelConfig,
},
};
session.topic = mask.name; session.topic = mask.name;
} }
@@ -167,6 +205,13 @@ export const useChatStore = create<ChatStore>()(
})); }));
}, },
nextSession(delta) {
const n = get().sessions.length;
const limit = (x: number) => (x + n) % n;
const i = get().currentSessionIndex;
get().selectSession(limit(i + delta));
},
deleteSession(index) { deleteSession(index) {
const deletingLastSession = get().sessions.length === 1; const deletingLastSession = get().sessions.length === 1;
const deletedSession = get().sessions.at(index); const deletedSession = get().sessions.at(index);
@@ -226,6 +271,7 @@ export const useChatStore = create<ChatStore>()(
onNewMessage(message) { onNewMessage(message) {
get().updateCurrentSession((session) => { get().updateCurrentSession((session) => {
session.messages = session.messages.concat();
session.lastUpdate = Date.now(); session.lastUpdate = Date.now();
}); });
get().updateStat(message); get().updateStat(message);
@@ -236,9 +282,12 @@ export const useChatStore = create<ChatStore>()(
const session = get().currentSession(); const session = get().currentSession();
const modelConfig = session.mask.modelConfig; const modelConfig = session.mask.modelConfig;
const userContent = fillTemplateWith(content, modelConfig);
console.log("[User Input] after template: ", userContent);
const userMessage: ChatMessage = createMessage({ const userMessage: ChatMessage = createMessage({
role: "user", role: "user",
content, content: userContent,
}); });
const botMessage: ChatMessage = createMessage({ const botMessage: ChatMessage = createMessage({
@@ -248,36 +297,25 @@ export const useChatStore = create<ChatStore>()(
model: modelConfig.model, model: modelConfig.model,
}); });
const systemInfo = createMessage({
role: "system",
content: `IMPORTANT: You are a virtual assistant powered by the ${
modelConfig.model
} model, now time is ${new Date().toLocaleString()}}`,
id: botMessage.id! + 1,
});
// get recent messages // get recent messages
const systemMessages = [];
// if user define a mask with context prompts, wont send system info
if (session.mask.context.length === 0) {
systemMessages.push(systemInfo);
}
const recentMessages = get().getMessagesWithMemory(); const recentMessages = get().getMessagesWithMemory();
const sendMessages = systemMessages.concat( const sendMessages = recentMessages.concat(userMessage);
recentMessages.concat(userMessage),
);
const sessionIndex = get().currentSessionIndex; const sessionIndex = get().currentSessionIndex;
const messageIndex = get().currentSession().messages.length + 1; const messageIndex = get().currentSession().messages.length + 1;
// save user's and bot's message // save user's and bot's message
get().updateCurrentSession((session) => { get().updateCurrentSession((session) => {
session.messages.push(userMessage); const savedUserMessage = {
session.messages.push(botMessage); ...userMessage,
content,
};
session.messages = session.messages.concat([
savedUserMessage,
botMessage,
]);
}); });
// make request // make request
console.log("[User Input] ", sendMessages);
api.llm.chat({ api.llm.chat({
messages: sendMessages, messages: sendMessages,
config: { ...modelConfig, stream: true }, config: { ...modelConfig, stream: true },
@@ -286,7 +324,9 @@ export const useChatStore = create<ChatStore>()(
if (message) { if (message) {
botMessage.content = message; botMessage.content = message;
} }
set(() => ({})); get().updateCurrentSession((session) => {
session.messages = session.messages.concat();
});
}, },
onFinish(message) { onFinish(message) {
botMessage.streaming = false; botMessage.streaming = false;
@@ -298,7 +338,6 @@ export const useChatStore = create<ChatStore>()(
sessionIndex, sessionIndex,
botMessage.id ?? messageIndex, botMessage.id ?? messageIndex,
); );
set(() => ({}));
}, },
onError(error) { onError(error) {
const isAborted = error.message.includes("aborted"); const isAborted = error.message.includes("aborted");
@@ -311,8 +350,9 @@ export const useChatStore = create<ChatStore>()(
botMessage.streaming = false; botMessage.streaming = false;
userMessage.isError = !isAborted; userMessage.isError = !isAborted;
botMessage.isError = !isAborted; botMessage.isError = !isAborted;
get().updateCurrentSession((session) => {
set(() => ({})); session.messages = session.messages.concat();
});
ChatControllerPool.remove( ChatControllerPool.remove(
sessionIndex, sessionIndex,
botMessage.id ?? messageIndex, botMessage.id ?? messageIndex,
@@ -347,53 +387,84 @@ export const useChatStore = create<ChatStore>()(
getMessagesWithMemory() { getMessagesWithMemory() {
const session = get().currentSession(); const session = get().currentSession();
const modelConfig = session.mask.modelConfig; const modelConfig = session.mask.modelConfig;
const clearContextIndex = session.clearContextIndex ?? 0;
const messages = session.messages.slice();
const totalMessageCount = session.messages.length;
// wont send cleared context messages // in-context prompts
const clearedContextMessages = session.messages.slice( const contextPrompts = session.mask.context.slice();
session.clearContextIndex ?? 0,
);
const messages = clearedContextMessages.filter((msg) => !msg.isError);
const n = messages.length;
const context = session.mask.context.slice(); // system prompts, to get close to OpenAI Web ChatGPT
// only will be injected if user does not use a mask or set none context prompts
// long term memory const shouldInjectSystemPrompts = contextPrompts.length === 0;
if ( const systemPrompts = shouldInjectSystemPrompts
modelConfig.sendMemory && ? [
session.memoryPrompt && createMessage({
session.memoryPrompt.length > 0 role: "system",
) { content: fillTemplateWith("", {
const memoryPrompt = get().getMemoryPrompt(); ...modelConfig,
context.push(memoryPrompt); template: DEFAULT_SYSTEM_TEMPLATE,
}),
}),
]
: [];
if (shouldInjectSystemPrompts) {
console.log(
"[Global System Prompt] ",
systemPrompts.at(0)?.content ?? "empty",
);
} }
// get short term and unmemoried long term memory // long term memory
const shortTermMemoryMessageIndex = Math.max( const shouldSendLongTermMemory =
0, modelConfig.sendMemory &&
n - modelConfig.historyMessageCount, session.memoryPrompt &&
); session.memoryPrompt.length > 0 &&
const longTermMemoryMessageIndex = session.lastSummarizeIndex; session.lastSummarizeIndex <= clearContextIndex;
const mostRecentIndex = Math.max( const longTermMemoryPrompts = shouldSendLongTermMemory
shortTermMemoryMessageIndex, ? [get().getMemoryPrompt()]
longTermMemoryMessageIndex, : [];
); const longTermMemoryStartIndex = session.lastSummarizeIndex;
const threshold = modelConfig.compressMessageLengthThreshold * 2;
// get recent messages as many as possible // short term memory
const shortTermMemoryStartIndex = Math.max(
0,
totalMessageCount - modelConfig.historyMessageCount,
);
// lets concat send messages, including 4 parts:
// 0. system prompt: to get close to OpenAI Web ChatGPT
// 1. long term memory: summarized memory messages
// 2. pre-defined in-context prompts
// 3. short term memory: latest n messages
// 4. newest input message
const memoryStartIndex = shouldSendLongTermMemory
? Math.min(longTermMemoryStartIndex, shortTermMemoryStartIndex)
: shortTermMemoryStartIndex;
// and if user has cleared history messages, we should exclude the memory too.
const contextStartIndex = Math.max(clearContextIndex, memoryStartIndex);
const maxTokenThreshold = modelConfig.max_tokens;
// get recent messages as much as possible
const reversedRecentMessages = []; const reversedRecentMessages = [];
for ( for (
let i = n - 1, count = 0; let i = totalMessageCount - 1, tokenCount = 0;
i >= mostRecentIndex && count < threshold; i >= contextStartIndex && tokenCount < maxTokenThreshold;
i -= 1 i -= 1
) { ) {
const msg = messages[i]; const msg = messages[i];
if (!msg || msg.isError) continue; if (!msg || msg.isError) continue;
count += msg.content.length; tokenCount += estimateTokenLength(msg.content);
reversedRecentMessages.push(msg); reversedRecentMessages.push(msg);
} }
// concat // concat all messages
const recentMessages = context.concat(reversedRecentMessages.reverse()); const recentMessages = [
...systemPrompts,
...longTermMemoryPrompts,
...contextPrompts,
...reversedRecentMessages.reverse(),
];
return recentMessages; return recentMessages;
}, },

View File

@@ -1,6 +1,7 @@
import { create } from "zustand"; import { create } from "zustand";
import { persist } from "zustand/middleware"; import { persist } from "zustand/middleware";
import { StoreKey } from "../constant"; import { getClientConfig } from "../config/client";
import { DEFAULT_INPUT_TEMPLATE, StoreKey } from "../constant";
export enum SubmitKey { export enum SubmitKey {
Enter = "Enter", Enter = "Enter",
@@ -21,7 +22,7 @@ export const DEFAULT_CONFIG = {
avatar: "1f603", avatar: "1f603",
fontSize: 14, fontSize: 14,
theme: Theme.Auto as Theme, theme: Theme.Auto as Theme,
tightBorder: false, tightBorder: !!getClientConfig()?.isApp,
sendPreviewBubble: true, sendPreviewBubble: true,
sidebarWidth: 300, sidebarWidth: 300,
@@ -34,9 +35,11 @@ export const DEFAULT_CONFIG = {
temperature: 0.5, temperature: 0.5,
max_tokens: 2000, max_tokens: 2000,
presence_penalty: 0, presence_penalty: 0,
frequency_penalty: 0,
sendMemory: true, sendMemory: true,
historyMessageCount: 4, historyMessageCount: 4,
compressMessageLengthThreshold: 1000, compressMessageLengthThreshold: 1000,
template: DEFAULT_INPUT_TEMPLATE,
}, },
}; };
@@ -60,6 +63,10 @@ export const ALL_MODELS = [
name: "gpt-4-0314", name: "gpt-4-0314",
available: ENABLE_GPT4, available: ENABLE_GPT4,
}, },
{
name: "gpt-4-0613",
available: ENABLE_GPT4,
},
{ {
name: "gpt-4-32k", name: "gpt-4-32k",
available: ENABLE_GPT4, available: ENABLE_GPT4,
@@ -69,13 +76,9 @@ export const ALL_MODELS = [
available: ENABLE_GPT4, available: ENABLE_GPT4,
}, },
{ {
name: "gpt-4-mobile", name: "gpt-4-32k-0613",
available: ENABLE_GPT4, available: ENABLE_GPT4,
}, },
{
name: "text-davinci-002-render-sha-mobile",
available: true,
},
{ {
name: "gpt-3.5-turbo", name: "gpt-3.5-turbo",
available: true, available: true,
@@ -84,6 +87,18 @@ export const ALL_MODELS = [
name: "gpt-3.5-turbo-0301", name: "gpt-3.5-turbo-0301",
available: true, available: true,
}, },
{
name: "gpt-3.5-turbo-0613",
available: true,
},
{
name: "gpt-3.5-turbo-16k",
available: true,
},
{
name: "gpt-3.5-turbo-16k-0613",
available: true,
},
{ {
name: "qwen-v1", // 通义千问 name: "qwen-v1", // 通义千问
available: false, available: false,
@@ -124,7 +139,7 @@ export function limitNumber(
export function limitModel(name: string) { export function limitModel(name: string) {
return ALL_MODELS.some((m) => m.name === name && m.available) return ALL_MODELS.some((m) => m.name === name && m.available)
? name ? name
: ALL_MODELS[4].name; : "gpt-3.5-turbo";
} }
export const ModalConfigValidator = { export const ModalConfigValidator = {
@@ -137,6 +152,9 @@ export const ModalConfigValidator = {
presence_penalty(x: number) { presence_penalty(x: number) {
return limitNumber(x, -2, 2, 0); return limitNumber(x, -2, 2, 0);
}, },
frequency_penalty(x: number) {
return limitNumber(x, -2, 2, 0);
},
temperature(x: number) { temperature(x: number) {
return limitNumber(x, 0, 1, 1); return limitNumber(x, 0, 1, 1);
}, },
@@ -159,14 +177,16 @@ export const useAppConfig = create<ChatConfigStore>()(
}), }),
{ {
name: StoreKey.Config, name: StoreKey.Config,
version: 2, version: 3.2,
migrate(persistedState, version) { migrate(persistedState, version) {
if (version === 2) return persistedState as any; if (version === 3.2) return persistedState as any;
const state = persistedState as ChatConfig; const state = persistedState as ChatConfig;
state.modelConfig.sendMemory = true; state.modelConfig.sendMemory = true;
state.modelConfig.historyMessageCount = 4; state.modelConfig.historyMessageCount = 4;
state.modelConfig.compressMessageLengthThreshold = 1000; state.modelConfig.compressMessageLengthThreshold = 1000;
state.modelConfig.frequency_penalty = 0;
state.modelConfig.template = DEFAULT_INPUT_TEMPLATE;
state.dontShowMaskSplashScreen = false; state.dontShowMaskSplashScreen = false;
return state; return state;

View File

@@ -3,7 +3,7 @@ import { persist } from "zustand/middleware";
import { BUILTIN_MASKS } from "../masks"; import { BUILTIN_MASKS } from "../masks";
import { getLang, Lang } from "../locales"; import { getLang, Lang } from "../locales";
import { DEFAULT_TOPIC, ChatMessage } from "./chat"; import { DEFAULT_TOPIC, ChatMessage } from "./chat";
import { ModelConfig, ModelType, useAppConfig } from "./config"; import { ModelConfig, useAppConfig } from "./config";
import { StoreKey } from "../constant"; import { StoreKey } from "../constant";
export type Mask = { export type Mask = {
@@ -89,7 +89,18 @@ export const useMaskStore = create<MaskStore>()(
const userMasks = Object.values(get().masks).sort( const userMasks = Object.values(get().masks).sort(
(a, b) => b.id - a.id, (a, b) => b.id - a.id,
); );
return userMasks.concat(BUILTIN_MASKS); const config = useAppConfig.getState();
const buildinMasks = BUILTIN_MASKS.map(
(m) =>
({
...m,
modelConfig: {
...config.modelConfig,
...m.modelConfig,
},
} as Mask),
);
return userMasks.concat(buildinMasks);
}, },
search(text) { search(text) {
return Object.values(get().masks); return Object.values(get().masks);

87
app/store/sync.ts Normal file
View File

@@ -0,0 +1,87 @@
import { Updater } from "../typing";
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { StoreKey } from "../constant";
export interface WebDavConfig {
server: string;
username: string;
password: string;
}
export interface SyncStore {
webDavConfig: WebDavConfig;
lastSyncTime: number;
update: Updater<WebDavConfig>;
check: () => Promise<boolean>;
path: (path: string) => string;
headers: () => { Authorization: string };
}
const FILE = {
root: "/chatgpt-next-web/",
};
export const useSyncStore = create<SyncStore>()(
persist(
(set, get) => ({
webDavConfig: {
server: "",
username: "",
password: "",
},
lastSyncTime: 0,
update(updater) {
const config = { ...get().webDavConfig };
updater(config);
set({ webDavConfig: config });
},
async check() {
try {
const res = await fetch(this.path(""), {
method: "PROFIND",
headers: this.headers(),
});
console.log(res);
return res.status === 207;
} catch (e) {
console.error("[Sync] ", e);
return false;
}
},
path(path: string) {
let url = get().webDavConfig.server;
if (!url.endsWith("/")) {
url += "/";
}
if (path.startsWith("/")) {
path = path.slice(1);
}
return url + path;
},
headers() {
const auth = btoa(
[get().webDavConfig.username, get().webDavConfig.password].join(":"),
);
return {
Authorization: `Basic ${auth}`,
};
},
}),
{
name: StoreKey.Sync,
version: 1,
},
),
);

View File

@@ -2,7 +2,7 @@ import { create } from "zustand";
import { persist } from "zustand/middleware"; import { persist } from "zustand/middleware";
import { FETCH_COMMIT_URL, StoreKey } from "../constant"; import { FETCH_COMMIT_URL, StoreKey } from "../constant";
import { api } from "../client/api"; import { api } from "../client/api";
import { showToast } from "../components/ui-lib"; import { getClientConfig } from "../config/client";
export interface UpdateStore { export interface UpdateStore {
lastUpdate: number; lastUpdate: number;
@@ -17,20 +17,6 @@ export interface UpdateStore {
updateUsage: (force?: boolean) => Promise<void>; updateUsage: (force?: boolean) => Promise<void>;
} }
function queryMeta(key: string, defaultValue?: string): string {
let ret: string;
if (document) {
const meta = document.head.querySelector(
`meta[name='${key}']`,
) as HTMLMetaElement;
ret = meta?.content ?? "";
} else {
ret = defaultValue ?? "";
}
return ret;
}
const ONE_MINUTE = 60 * 1000; const ONE_MINUTE = 60 * 1000;
export const useUpdateStore = create<UpdateStore>()( export const useUpdateStore = create<UpdateStore>()(
@@ -44,7 +30,7 @@ export const useUpdateStore = create<UpdateStore>()(
version: "unknown", version: "unknown",
async getLatestVersion(force = false) { async getLatestVersion(force = false) {
set(() => ({ version: queryMeta("version") ?? "unknown" })); set(() => ({ version: getClientConfig()?.commitId ?? "unknown" }));
const overTenMins = Date.now() - get().lastUpdate > 10 * ONE_MINUTE; const overTenMins = Date.now() - get().lastUpdate > 10 * ONE_MINUTE;
if (!force && !overTenMins) return; if (!force && !overTenMins) return;
@@ -84,7 +70,7 @@ export const useUpdateStore = create<UpdateStore>()(
})); }));
} }
} catch (e) { } catch (e) {
showToast((e as Error).message); console.error((e as Error).message);
} }
}, },
}), }),

View File

@@ -304,6 +304,9 @@ pre {
&:hover { &:hover {
filter: brightness(0.9); filter: brightness(0.9);
} }
&:focus {
filter: brightness(0.95);
}
} }
.error { .error {

View File

@@ -844,6 +844,7 @@
font-size: 85%; font-size: 85%;
line-height: 1.45; line-height: 1.45;
border-radius: 6px; border-radius: 6px;
direction: ltr;
} }
.markdown-body pre code, .markdown-body pre code,
@@ -1116,4 +1117,16 @@
.markdown-body ::-webkit-calendar-picker-indicator { .markdown-body ::-webkit-calendar-picker-indicator {
filter: invert(50%); filter: invert(50%);
} }
.markdown-body .mermaid {
border: var(--border-in-light);
margin-bottom: 10px;
border-radius: 4px;
padding: 10px;
background-color: var(--white);
}
#dmermaid {
display: none;
}

View File

@@ -24,7 +24,6 @@
.window-header-sub-title { .window-header-sub-title {
font-size: 14px; font-size: 14px;
margin-top: 5px;
} }
} }
@@ -32,6 +31,6 @@
display: inline-flex; display: inline-flex;
} }
.window-action-button { .window-action-button:not(:first-child) {
margin-left: 10px; margin-left: 10px;
} }

View File

@@ -8,7 +8,12 @@ export function trimTopic(topic: string) {
export async function copyToClipboard(text: string) { export async function copyToClipboard(text: string) {
try { try {
await navigator.clipboard.writeText(text); if (window.__TAURI__) {
window.__TAURI__.writeText(text);
} else {
await navigator.clipboard.writeText(text);
}
showToast(Locale.Copy.Success); showToast(Locale.Copy.Success);
} catch (error) { } catch (error) {
const textArea = document.createElement("textarea"); const textArea = document.createElement("textarea");
@@ -152,6 +157,7 @@ export function autoGrowTextArea(dom: HTMLTextAreaElement) {
const width = getDomContentWidth(dom); const width = getDomContentWidth(dom);
measureDom.style.width = width + "px"; measureDom.style.width = width + "px";
measureDom.innerText = dom.value !== "" ? dom.value : "1"; measureDom.innerText = dom.value !== "" ? dom.value : "1";
measureDom.style.fontSize = dom.style.fontSize;
const endWithEmptyLine = dom.value.endsWith("\n"); const endWithEmptyLine = dom.value.endsWith("\n");
const height = parseFloat(window.getComputedStyle(measureDom).height); const height = parseFloat(window.getComputedStyle(measureDom).height);
const singleLineHeight = parseFloat( const singleLineHeight = parseFloat(

22
app/utils/token.ts Normal file
View File

@@ -0,0 +1,22 @@
export function estimateTokenLength(input: string): number {
let tokenLength = 0;
for (let i = 0; i < input.length; i++) {
const charCode = input.charCodeAt(i);
if (charCode < 128) {
// ASCII character
if (charCode <= 122 && charCode >= 65) {
// a-Z
tokenLength += 0.25;
} else {
tokenLength += 0.5;
}
} else {
// Unicode character
tokenLength += 1.5;
}
}
return tokenLength;
}

View File

@@ -12,7 +12,7 @@ services:
- BASE_URL=$BASE_URL - BASE_URL=$BASE_URL
- OPENAI_ORG_ID=$OPENAI_ORG_ID - OPENAI_ORG_ID=$OPENAI_ORG_ID
- HIDE_USER_API_KEY=$HIDE_USER_API_KEY - HIDE_USER_API_KEY=$HIDE_USER_API_KEY
- DISABLE_GPT4=DISABLE_GPT4 - DISABLE_GPT4=$DISABLE_GPT4
chatgpt-next-web-proxy: chatgpt-next-web-proxy:
profiles: ["proxy"] profiles: ["proxy"]
@@ -27,4 +27,4 @@ services:
- BASE_URL=$BASE_URL - BASE_URL=$BASE_URL
- OPENAI_ORG_ID=$OPENAI_ORG_ID - OPENAI_ORG_ID=$OPENAI_ORG_ID
- HIDE_USER_API_KEY=$HIDE_USER_API_KEY - HIDE_USER_API_KEY=$HIDE_USER_API_KEY
- DISABLE_GPT4=DISABLE_GPT4 - DISABLE_GPT4=$DISABLE_GPT4

View File

@@ -0,0 +1,37 @@
# Guía de implementación de Cloudflare Pages
## Cómo crear un nuevo proyecto
Bifurca el proyecto en Github, luego inicia sesión en dash.cloudflare.com y ve a Pages.
1. Haga clic en "Crear un proyecto".
2. Selecciona Conectar a Git.
3. Vincula páginas de Cloudflare a tu cuenta de GitHub.
4. Seleccione este proyecto que bifurcó.
5. Haga clic en "Comenzar configuración".
6. Para "Nombre del proyecto" y "Rama de producción", puede utilizar los valores predeterminados o cambiarlos según sea necesario.
7. En Configuración de compilación, seleccione la opción Ajustes preestablecidos de Framework y seleccione Siguiente.js.
8. Debido a los errores de node:buffer, no use el "comando Construir" predeterminado por ahora. Utilice el siguiente comando:
npx https://prerelease-registry.devprod.cloudflare.dev/next-on-pages/runs/4930842298/npm-package-next-on-pages-230 --experimental-minify
9. Para "Generar directorio de salida", utilice los valores predeterminados y no los modifique.
10. No modifique el "Directorio raíz".
11. Para "Variables de entorno", haga clic en ">" y luego haga clic en "Agregar variable". Rellene la siguiente información:
* `NODE_VERSION=20.1`
* `NEXT_TELEMETRY_DISABLE=1`
* `OPENAI_API_KEY=你自己的API Key`
* `YARN_VERSION=1.22.19`
* `PHP_VERSION=7.4`
Dependiendo de sus necesidades reales, puede completar opcionalmente las siguientes opciones:
* `CODE= 可选填,访问密码,可以使用逗号隔开多个密码`
* `OPENAI_ORG_ID= 可选填,指定 OpenAI 中的组织 ID`
* `HIDE_USER_API_KEY=1 可选,不让用户自行填入 API Key`
* `DISABLE_GPT4=1 可选,不让用户使用 GPT-4`
12. Haga clic en "Guardar e implementar".
13. Haga clic en "Cancelar implementación" porque necesita rellenar los indicadores de compatibilidad.
14. Vaya a "Configuración de compilación", "Funciones" y busque "Indicadores de compatibilidad".
15. Rellene "nodejs_compat" en "Configurar indicador de compatibilidad de producción" y "Configurar indicador de compatibilidad de vista previa".
16. Vaya a "Implementaciones" y haga clic en "Reintentar implementación".
17. Disfrutar.

205
docs/faq-es.md Normal file
View File

@@ -0,0 +1,205 @@
# Preguntas frecuentes
## ¿Cómo puedo obtener ayuda rápidamente?
1. Pregunte a ChatGPT / Bing / Baidu / Google, etc.
2. Pregunte a los internautas. Sírvase proporcionar información general sobre el problema y una descripción detallada del problema encontrado. Las preguntas de alta calidad facilitan la obtención de respuestas útiles.
# Problemas relacionados con la implementación
Referencia tutorial detallada para varios métodos de implementación: https://rptzik3toh.feishu.cn/docx/XtrdduHwXoSCGIxeFLlcEPsdn8b
## ¿Por qué la versión de implementación de Docker sigue solicitando actualizaciones?
La versión de Docker es equivalente a la versión estable, la última versión de Docker es siempre la misma que la última versión de lanzamiento, y la frecuencia de lanzamiento actual es de uno a dos días, por lo que la versión de Docker siempre se retrasará con respecto a la última confirmación de uno a dos días, lo que se espera.
## Cómo implementar en Vercel
1. Regístrese para obtener una cuenta de Github y bifurque el proyecto
2. Regístrese en Vercel (se requiere verificación de teléfono móvil, puede usar un número chino) y conéctese a su cuenta de Github
3. Cree un nuevo proyecto en Vercel, seleccione el proyecto que bifurcó en Github, complete las variables de entorno según sea necesario e inicie la implementación. Después de la implementación, puede acceder a su proyecto a través del nombre de dominio proporcionado por Vercel con una escalera.
4. Si necesitas acceder sin muros en China: En tu sitio web de administración de dominios, agrega un registro CNAME para tu nombre de dominio que apunte a cname.vercel-dns.com. Después de eso, configure el acceso a su dominio en Vercel.
## Cómo modificar las variables de entorno de Vercel
* Vaya a la página de la consola de Vercel;
* Seleccione su siguiente proyecto web chatgpt;
* Haga clic en la opción Configuración en el encabezado de la página;
* Busque la opción Variables de entorno en la barra lateral;
* Modifique el valor correspondiente.
## ¿Qué es la variable de entorno CODE? ¿Es obligatorio configurar?
Esta es su contraseña de acceso personalizada, puede elegir:
1. Si no es así, elimine la variable de entorno. Precaución: Cualquier persona puede acceder a tu proyecto en este momento.
2. Cuando implemente el proyecto, establezca la variable de entorno CODE (admite varias comas de contraseña separadas). Después de establecer la contraseña de acceso, debe ingresar la contraseña de acceso en la interfaz de configuración antes de poder usarla. Ver[Instrucciones relacionadas](https://github.com/Yidadaa/ChatGPT-Next-Web/blob/main/README_CN.md#%E9%85%8D%E7%BD%AE%E9%A1%B5%E9%9D%A2%E8%AE%BF%E9%97%AE%E5%AF%86%E7%A0%81)
## ¿Por qué la versión que implementé no transmite respuestas?
> Debates relacionados:[#386](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/386)
Si utiliza el proxy inverso ngnix, debe agregar el siguiente código al archivo de configuración:
# 不缓存,支持流式输出
proxy_cache off; # 关闭缓存
proxy_buffering off; # 关闭代理缓冲
chunked_transfer_encoding on; # 开启分块传输编码
tcp_nopush on; # 开启TCP NOPUSH选项禁止Nagle算法
tcp_nodelay on; # 开启TCP NODELAY选项禁止延迟ACK算法
keepalive_timeout 300; # 设定keep-alive超时时间为65秒
Si está implementando en Netlify y este problema aún está pendiente de resolución, tenga paciencia.
## Lo implementé, pero no puedo acceder a él
Marque para descartar los siguientes problemas:
* ¿Se ha iniciado el servicio?
* ¿Los puertos están asignados correctamente?
* ¿El firewall está abriendo puertos?
* ¿Es transitable la ruta al servidor?
* ¿Se resuelve correctamente el nombre de dominio?
## ¿Qué es un proxy y cómo lo uso?
Debido a las restricciones de IP de OpenAI, China y algunos otros países no pueden conectarse directamente a las API de OpenAI y necesitan pasar por un proxy. Puede usar un servidor proxy (proxy de reenvío) o un proxy inverso de API OpenAI ya configurado.
* Ejemplo de agente positivo: escalera científica de Internet. En el caso de la implementación de Docker, establezca la variable de entorno HTTP_PROXY en su dirección proxy (por ejemplo: 10.10.10.10:8002).
* Ejemplo de proxy inverso: puede usar una dirección proxy creada por otra persona o configurarla de forma gratuita a través de Cloudflare. Establezca la variable de entorno del proyecto BASE_URL en su dirección proxy.
## ¿Se pueden implementar servidores domésticos?
Sí, pero hay que resolverlo:
* Requiere un proxy para conectarse a sitios como GitHub y openAI;
* Si el servidor doméstico desea configurar la resolución de nombres de dominio, debe registrarse;
* Las políticas nacionales restringen el acceso proxy a las aplicaciones relacionadas con Internet/ChatGPT y pueden bloquearse.
## ¿Por qué recibo un error de red después de la implementación de Docker?
Ver Discusión: https://github.com/Yidadaa/ChatGPT-Next-Web/issues/1569 para más detalles
# Problemas relacionados con el uso
## ¿Por qué sigues diciendo "Algo salió mal, inténtalo de nuevo más tarde"?
Puede haber muchas razones, por favor solucione los problemas en orden:
* Compruebe primero si la versión del código es la última versión, actualice a la última versión e inténtelo de nuevo;
* Compruebe si la clave API está configurada correctamente y si el nombre de la variable de entorno debe estar en mayúsculas y subrayado;
* Compruebe si la clave API está disponible;
* Si aún no puede identificar el problema después de los pasos anteriores, envíe un nuevo problema en el campo de problema con el registro de tiempo de ejecución de Verbel o el registro de tiempo de ejecución de Docker.
## ¿Por qué la respuesta de ChatGPT es confusa?
Interfaz de configuración: uno de los elementos de configuración del modelo es `temperature`, si este valor es mayor que 1, entonces existe el riesgo de una respuesta confusa, simplemente vuelva a llamarlo a dentro de 1.
## Al usarlo, aparece "Ahora en un estado no autorizado, ingrese la contraseña de acceso en la pantalla de configuración"?
El proyecto establece la contraseña de acceso a través de la variable de entorno CODE. Cuando lo use por primera vez, debe ingresar el código de acceso en la configuración para usarlo.
## Use el mensaje "Excedió su cuota actual, ..."
Hay un problema con la API KEY. Saldo insuficiente.
# Problemas relacionados con el servicio de red
## ¿Qué es Cloudflare?
Cloudflare (CF) es un proveedor de servicios de red que proporciona CDN, administración de nombres de dominio, alojamiento de páginas estáticas, implementación de funciones de computación perimetral y más. Usos comunes: comprar y/o alojar su nombre de dominio (resolución, nombre de dominio dinámico, etc.), poner un CDN en su servidor (puede ocultar la IP de la pared), desplegar un sitio web (CF Pages). CF ofrece la mayoría de los servicios de forma gratuita.
## ¿Qué es Vercel?
Vercel es una plataforma global en la nube diseñada para ayudar a los desarrolladores a crear e implementar aplicaciones web modernas más rápido. Este proyecto, junto con muchas aplicaciones web, se puede implementar en Vercel de forma gratuita con un solo clic. Sin código, sin Linux, sin servidores, sin tarifas, sin agente API OpenAI. La desventaja es que necesita vincular el nombre de dominio para poder acceder a él sin muros en China.
## ¿Cómo obtengo un nombre de dominio?
1. Vaya al proveedor de nombres de dominio para registrarse, hay Namesilo (soporte Alipay), Cloudflare, etc. en el extranjero, y hay Wanwang en China;
2. Proveedores de nombres de dominio gratuitos: eu.org (nombre de dominio de segundo nivel), etc.;
3. Pídale a un amigo un nombre de dominio de segundo nivel gratuito.
## Cómo obtener un servidor
* Ejemplos de proveedores de servidores extranjeros: Amazon Cloud, Google Cloud, Vultr, Bandwagon, Hostdare, etc.
Asuntos de servidores extranjeros: Las líneas de servidor afectan las velocidades de acceso nacional, se recomiendan los servidores de línea CN2 GIA y CN2. Si el servidor es de difícil acceso en China (pérdida grave de paquetes, etc.), puede intentar configurar un CDN (Cloudflare y otros proveedores).
* Proveedores de servidores nacionales: Alibaba Cloud, Tencent, etc.;
Asuntos de servidores nacionales: La resolución de nombres de dominio requiere la presentación de ICP; El ancho de banda del servidor doméstico es más caro; El acceso a sitios web extranjeros (Github, openAI, etc.) requiere un proxy.
## ¿En qué circunstancias debe grabarse el servidor?
Los sitios web que operan en China continental deben presentar de acuerdo con los requisitos reglamentarios. En la práctica, si el servidor está ubicado en China y hay resolución de nombres de dominio, el proveedor del servidor implementará los requisitos reglamentarios de presentación, de lo contrario el servicio se cerrará. Las reglas habituales son las siguientes:
|ubicación del servidor|proveedor de nombres de dominio|si se requiere la presentación|
|---|---|---|
|Doméstico|Doméstico|Sí|
|nacional|extranjero|sí|
|extranjero|extranjero|no|
|extranjero|nacional|normalmente no|
Después de cambiar de proveedor de servidores, debe transferir la presentación de ICP.
# Problemas relacionados con OpenAI
## ¿Cómo registro una cuenta OpenAI?
Vaya a chat.openai.com para registrarse. Es necesario:
* Una buena escalera (OpenAI admite direcciones IP nativas regionales)
* Un buzón compatible (por ejemplo, Gmail o trabajo/escuela, no buzón de Outlook o QQ)
* Cómo recibir autenticación por SMS (por ejemplo, sitio web de activación de SMS)
## ¿Cómo activo la API de OpenAI? ¿Cómo verifico mi saldo de API?
Dirección del sitio web oficial (se requiere escalera): https://platform.openai.com/account/usage
Algunos internautas han construido un agente de consulta de saldo sin escalera, por favor pídales a los internautas que lo obtengan. Identifique si la fuente es confiable para evitar la fuga de la clave API.
## ¿Por qué mi cuenta OpenAI recién registrada no tiene un saldo API?
(Actualizado el 6 de abril) Las cuentas recién registradas suelen mostrar el saldo de la API después de 24 horas. Se otorga un saldo de $ 5 a una cuenta recién registrada.
## ¿Cómo puedo recargar la API de OpenAI?
OpenAI solo acepta tarjetas de crédito en regiones seleccionadas (no se pueden usar tarjetas de crédito chinas). Algunos ejemplos de avenidas son:
1. Depay tarjeta de crédito virtual
2. Solicitar una tarjeta de crédito extranjera
3. Encuentra a alguien para cobrar en línea
## ¿Cómo utilizo el acceso a la API de GPT-4?
* El acceso a la API para GPT-4 requiere una solicitud independiente. Ingrese a la cola de la solicitud completando su información en la lista de espera (prepare su ID de organización OpenAI): https://openai.com/waitlist/gpt-4-api
Espere el mensaje de correo después.
* Habilitar ChatGPT Plus no significa permisos GPT-4, y los dos no tienen nada que ver entre sí.
## Uso de la interfaz de Azure OpenAI
Por favor consulte:[#371](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/371)
## ¿Por qué mi token se agota tan rápido?
> Debates relacionados:[#518](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/518)
* Si tiene permisos de GPT 4 y usa las API de GPT 4 a diario, el monto de su factura aumentará rápidamente porque el precio de GPT 4 es aproximadamente 15 veces mayor que el de GPT 3.5;
* Si está usando GPT 3.5 y no lo usa con mucha frecuencia y aún nota que su factura aumenta rápidamente, siga estos pasos para solucionar problemas ahora:
* Vaya al sitio web oficial de OpenAI para verificar sus registros de consumo de API Key, si su token se consume cada hora y se consumen decenas de miles de tokens cada vez, entonces su clave debe haberse filtrado, elimine y regenere inmediatamente.**No verifique su saldo en un sitio web desordenado.**
* Si su contraseña se acorta, como letras dentro de 5 dígitos, entonces el costo de voladura es muy bajo, se recomienda que busque en el registro de Docker para ver si alguien ha probado muchas combinaciones de contraseñas, palabra clave: got access code
* A través de los dos métodos anteriores, puede localizar la razón por la cual su token se consume rápidamente:
* Si el registro de consumo de OpenAI es anormal, pero no hay ningún problema con el registro de Docker, entonces la clave API se filtra;
* Si el registro de Docker encuentra una gran cantidad de registros de código de acceso de obtención, entonces la contraseña ha sido destruida.
## ¿Cómo se facturan las API?
Instrucciones de facturación del sitio web de OpenAI: https://openai.com/pricing#language-models\
OpenAI cobra en función del número de tokens, y 1,000 tokens generalmente representan 750 palabras en inglés o 500 caracteres chinos. Prompt y Completion cuentan los costos por separado.\
|Modelo|Facturación de entrada de usuario (aviso)|Facturación de salida del modelo (finalización)|Número máximo de tokens por interacción|
|----|----|----|----|
|gpt-3.5|$0.002 / 1 mil tokens|$0.002 / 1 mil tokens|4096|
|gpt-4|$0.03 / 1 mil tokens|$0.06 / 1 mil tokens|8192|
|gpt-4-32K|$0.06 / 1 mil tokens|$0.12 / 1 mil tokens|32768|
## ¿Cuál es la diferencia entre los modelos GPT-3.5-TURBO y GPT3.5-TURBO-0301 (o GPT3.5-TURBO-MMDD)?
Descripción de la documentación oficial: https://platform.openai.com/docs/models/gpt-3-5
* GPT-3.5-Turbo es el último modelo y se actualiza constantemente.
* GPT-3.5-turbo-0301 es una instantánea del modelo congelada el 1 de marzo, no cambiará y se espera que sea reemplazada por una nueva instantánea en 3 meses.

View File

@@ -14,7 +14,7 @@
![vercel-create-3](./images/vercel/vercel-create-3.jpg) ![vercel-create-3](./images/vercel/vercel-create-3.jpg)
1. 在项目配置页,点开 Environmane Variables 开始配置环境变量; 1. 在项目配置页,点开 Environmane Variables 开始配置环境变量;
2. 依次新增名为 OPENAI_API_KEY 和 CODE 的环境变量; 2. 依次新增名为 OPENAI_API_KEY 和 CODE ([访问密码](https://github.com/Yidadaa/ChatGPT-Next-Web/blob/357296986609c14de10bf210871d30e2f67a8784/docs/faq-cn.md#%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F-code-%E6%98%AF%E4%BB%80%E4%B9%88%E5%BF%85%E9%A1%BB%E8%AE%BE%E7%BD%AE%E5%90%97)) 的环境变量;
3. 填入环境变量对应的值; 3. 填入环境变量对应的值;
4. 点击 Add 确认增加环境变量; 4. 点击 Add 确认增加环境变量;
5. 请确保你添加了 OPENAI_API_KEY否则无法使用 5. 请确保你添加了 OPENAI_API_KEY否则无法使用
@@ -36,4 +36,4 @@
![vercel-redeploy](./images/vercel/vercel-redeploy.jpg) ![vercel-redeploy](./images/vercel/vercel-redeploy.jpg)
1. 进入 Vercel 项目内部控制台,点击顶部的 Deployments 按钮; 1. 进入 Vercel 项目内部控制台,点击顶部的 Deployments 按钮;
2. 选择列表最顶部一条的右侧按钮; 2. 选择列表最顶部一条的右侧按钮;
3. 点击 Redeploy 即可重新部署。 3. 点击 Redeploy 即可重新部署。

48
docs/vercel-es.md Normal file
View File

@@ -0,0 +1,48 @@
# Instrucciones de uso de Verbel
## Cómo crear un nuevo proyecto
Cuando bifurca este proyecto desde Github y necesita crear un nuevo proyecto de Vercel en Vercel para volver a implementarlo, debe seguir los pasos a continuación.
![vercel-create-1](./images/vercel/vercel-create-1.jpg)
1. Vaya a la página de inicio de la consola de Vercel;
2. Haga clic en Agregar nuevo;
3. Seleccione Proyecto.
![vercel-create-2](./images/vercel/vercel-create-2.jpg)
1. En Import Git Repository, busque chatgpt-next-web;
2. Seleccione el proyecto de la nueva bifurcación y haga clic en Importar.
![vercel-create-3](./images/vercel/vercel-create-3.jpg)
1. En la página de configuración del proyecto, haga clic en Variables de entorno para configurar las variables de entorno;
2. Agregar variables de entorno denominadas OPENAI_API_KEY y CODE;
3. Rellenar los valores correspondientes a las variables de entorno;
4. Haga clic en Agregar para confirmar la adición de variables de entorno;
5. Asegúrese de agregar OPENAI_API_KEY, de lo contrario no funcionará;
6. Haga clic en Implementar, créelo y espere pacientemente unos 5 minutos a que se complete la implementación.
## Cómo agregar un nombre de dominio personalizado
\[TODO]
## Cómo cambiar las variables de entorno
![vercel-env-edit](./images/vercel/vercel-env-edit.jpg)
1. Vaya a la consola interna del proyecto Vercel y haga clic en el botón Configuración en la parte superior;
2. Haga clic en Variables de entorno a la izquierda;
3. Haga clic en el botón a la derecha de una entrada existente;
4. Seleccione Editar para editarlo y, a continuación, guárdelo.
⚠️️ Nota: Lo necesita cada vez que modifique las variables de entorno[Volver a implementar el proyecto](#如何重新部署)para que los cambios surtan efecto!
## Cómo volver a implementar
![vercel-redeploy](./images/vercel/vercel-redeploy.jpg)
1. Vaya a la consola interna del proyecto Vercel y haga clic en el botón Implementaciones en la parte superior;
2. Seleccione el botón derecho del artículo superior de la lista;
3. Haga clic en Volver a implementar para volver a implementar.

View File

@@ -1,7 +1,48 @@
/** @type {import('next').NextConfig} */ const mode = process.env.BUILD_MODE ?? "standalone";
console.log("[Next] build mode", mode);
/** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
async rewrites() { webpack(config) {
config.module.rules.push({
test: /\.svg$/,
use: ["@svgr/webpack"],
});
return config;
},
output: mode,
images: {
unoptimized: mode === "export",
},
};
if (mode !== "export") {
nextConfig.headers = async () => {
return [
{
source: "/api/:path*",
headers: [
{ key: "Access-Control-Allow-Credentials", value: "true" },
{ key: "Access-Control-Allow-Origin", value: "*" },
{
key: "Access-Control-Allow-Methods",
value: "*",
},
{
key: "Access-Control-Allow-Headers",
value: "*",
},
{
key: "Access-Control-Max-Age",
value: "86400",
},
],
},
];
};
nextConfig.rewrites = async () => {
const ret = [ const ret = [
{ {
source: "/api/proxy/:path*", source: "/api/proxy/:path*",
@@ -29,16 +70,7 @@ const nextConfig = {
return { return {
beforeFiles: ret, beforeFiles: ret,
}; };
}, };
webpack(config) { }
config.module.rules.push({
test: /\.svg$/,
use: ["@svgr/webpack"],
});
return config;
},
output: "standalone",
};
export default nextConfig; export default nextConfig;

View File

@@ -1,13 +1,16 @@
{ {
"name": "chatgpt-next-web", "name": "chatgpt-next-web",
"version": "1.9.3",
"private": false, "private": false,
"license": "Anti 996", "license": "mit",
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "next lint",
"export": "cross-env BUILD_MODE=export BUILD_APP=1 yarn build",
"export:dev": "cross-env BUILD_MODE=export BUILD_APP=1 yarn dev",
"app:dev": "yarn tauri dev",
"app:build": "yarn tauri build",
"prompts": "node ./scripts/fetch-prompts.mjs", "prompts": "node ./scripts/fetch-prompts.mjs",
"prepare": "husky install", "prepare": "husky install",
"proxy-dev": "sh ./scripts/init-proxy.sh && proxychains -f ./scripts/proxychains.conf yarn dev" "proxy-dev": "sh ./scripts/init-proxy.sh && proxychains -f ./scripts/proxychains.conf yarn dev"
@@ -20,12 +23,12 @@
"emoji-picker-react": "^4.4.7", "emoji-picker-react": "^4.4.7",
"fuse.js": "^6.6.2", "fuse.js": "^6.6.2",
"html-to-image": "^1.11.11", "html-to-image": "^1.11.11",
"mermaid": "^10.1.0", "mermaid": "^10.2.3",
"next": "^13.4.3", "next": "^13.4.6",
"node-fetch": "^3.3.1", "node-fetch": "^3.3.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-markdown": "^8.0.5", "react-markdown": "^8.0.7",
"react-router-dom": "^6.10.0", "react-router-dom": "^6.10.0",
"rehype-highlight": "^6.0.0", "rehype-highlight": "^6.0.0",
"rehype-katex": "^6.0.2", "rehype-katex": "^6.0.2",
@@ -34,12 +37,13 @@
"remark-math": "^5.1.1", "remark-math": "^5.1.1",
"sass": "^1.59.2", "sass": "^1.59.2",
"spark-md5": "^3.0.2", "spark-md5": "^3.0.2",
"use-debounce": "^9.0.3", "use-debounce": "^9.0.4",
"zustand": "^4.3.6" "zustand": "^4.3.8"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^18.14.6", "@tauri-apps/cli": "^1.3.1",
"@types/react": "^18.0.28", "@types/node": "^20.3.1",
"@types/react": "^18.2.12",
"@types/react-dom": "^18.0.11", "@types/react-dom": "^18.0.11",
"@types/react-katex": "^3.0.0", "@types/react-katex": "^3.0.0",
"@types/spark-md5": "^3.0.2", "@types/spark-md5": "^3.0.2",
@@ -49,8 +53,11 @@
"eslint-config-prettier": "^8.8.0", "eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1", "eslint-plugin-prettier": "^4.2.1",
"husky": "^8.0.0", "husky": "^8.0.0",
"lint-staged": "^13.2.0", "lint-staged": "^13.2.2",
"prettier": "^2.8.7", "prettier": "^2.8.7",
"typescript": "4.9.5" "typescript": "4.9.5"
},
"resolutions": {
"lint-staged/yaml": "^2.2.2"
} }
} }

3
src-tauri/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
# Generated by Cargo
# will have compiled files and executables
/target/

3827
src-tauri/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

27
src-tauri/Cargo.toml Normal file
View File

@@ -0,0 +1,27 @@
[package]
name = "chatgpt-next-web"
version = "0.1.0"
description = "A cross platform app for LLM ChatBot."
authors = ["Yidadaa"]
license = "mit"
repository = ""
default-run = "chatgpt-next-web"
edition = "2021"
rust-version = "1.60"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "1.3.0", features = [] }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.3.0", features = ["clipboard-all", "dialog-all", "shell-open", "updater", "window-close", "window-hide", "window-maximize", "window-minimize", "window-set-icon", "window-set-ignore-cursor-events", "window-set-resizable", "window-show", "window-start-dragging", "window-unmaximize", "window-unminimize"] }
tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
[features]
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
# If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes.
# DO NOT REMOVE!!
custom-protocol = [ "tauri/custom-protocol" ]

3
src-tauri/build.rs Normal file
View File

@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

BIN
src-tauri/icons/128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
src-tauri/icons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

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