Compare commits

...

283 Commits
v1.1.0 ... main

Author SHA1 Message Date
liuweiqing
03c2ee2dd5 feat: vocechat客服聊天 2024-07-18 17:07:44 +08:00
liuweiqing
f3faf31925 chore: 去掉coze,由于收费 2024-07-16 12:12:30 +08:00
liuweiqing
c90d8ea7ac chore: 补上gpt-4选项 2024-07-04 09:37:49 +08:00
liuweiqing
9a19a8924d chore: gpt4因为coze收费不能用了 2024-07-03 22:17:42 +08:00
14790897
a8c5392616 fix: 如果api是旧域名就改为新域名 2024-05-11 08:39:02 +08:00
liuweiqing
8ad486d2f7 Merge branch 'main' of https://github.com/14790897/paper-ai 2024-04-24 07:48:26 +08:00
liuweiqing
cd4181c897 fix: 图片源替换 2024-04-24 07:48:25 +08:00
Shi Sheng
3868ad4acf
docs: 更新了中英文的README文件 (#39)
* Update README.md

* Update README.md

* Update README_en.md

* Update README.md

* Update README.md

* Update README.md

* Update README_en.md

* Update README.md

* Update README_en.md
2024-04-20 09:33:12 +08:00
Shi Sheng
ef7c85448e
docs: 更新了英文的README (#38)
* Update README_en.md

* Update README_en.md

* Update README_en.md

* Update README_en.md

* Update README_en.md
2024-04-20 08:14:28 +08:00
Shi Sheng
a3a5274ca5
chore: 把clone-and-run-locally换成了”克隆并在本地运行“(#37) 2024-04-19 12:41:47 +08:00
dependabot[bot]
f788b2222f
chore(deps): bump the npm_and_yarn group across 1 directory with 2 updates (#34)
Bumps the npm_and_yarn group with 2 updates in the / directory: [sweetalert2](https://github.com/sweetalert2/sweetalert2) and [follow-redirects](https://github.com/follow-redirects/follow-redirects).


Updates `sweetalert2` from 11.10.5 to 11.10.6
- [Release notes](https://github.com/sweetalert2/sweetalert2/releases)
- [Changelog](https://github.com/sweetalert2/sweetalert2/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sweetalert2/sweetalert2/compare/v11.10.5...v11.10.6)

Updates `follow-redirects` from 1.15.4 to 1.15.6
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.4...v1.15.6)

---
updated-dependencies:
- dependency-name: sweetalert2
  dependency-type: direct:production
  dependency-group: npm_and_yarn-security-group
- dependency-name: follow-redirects
  dependency-type: indirect
  dependency-group: npm_and_yarn-security-group
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-19 08:30:11 +08:00
Shi Sheng
1be61f7a9a
docs: 更新了英文翻译 (#36)
* Update README_en.md

* Update README_en.md

* Update README_en.md

* Update README_en.md

* Update README_en.md

* Update README_en.md

* Update README_en.md

* Update README_en.md

* Update README_en.md

* Update README_en.md
2024-04-19 08:27:24 +08:00
Shi Sheng
f2f9448f28
更新了README (#35)
docs: 更新readme的英文链接和证书描述
2024-04-18 21:39:20 +08:00
liuweiqing
dba8ec332a chore: remove强制设置 2024-04-16 14:34:59 +08:00
liuweiqing
b3c8dc1d4a feat: 增加commandr模型 2024-04-13 18:02:38 +08:00
liuweiqing
8b8702e04d chore: 强制更新我设置的api 2024-04-13 15:47:23 +08:00
liuweiqing
fe43130b4f chore: 更新api(因为原域名过期) 2024-04-13 15:33:17 +08:00
liuweiqing
a5ec62320d fix: 去除google sigin 2024-04-02 11:05:54 +08:00
liuweiqing
9b31268cda fix: linuxdo登录环境变量 2024-04-02 10:41:55 +08:00
14790897
29e2579b1d Merge branch 'main' of github.com:14790897/paper-ai 2024-04-01 15:40:00 +08:00
14790897
9ff111edaf fix: 本地开发需要环境变量 2024-04-01 15:39:51 +08:00
liuweiqing
01b26d9115 Merge branch 'main' of https://github.com/14790897/paper-ai 2024-03-30 09:46:04 +08:00
liuweiqing
4ce386e372 chore: 完善错误提示 2024-03-30 09:45:52 +08:00
14790897
9022969ac6 Revert "chore: 更新密钥"
This reverts commit d8cdb70333.
2024-03-29 08:54:39 +08:00
14790897
d227c78b5b Merge branch 'main' of github.com:14790897/paper-ai 2024-03-28 23:02:22 +08:00
14790897
d8cdb70333 chore: 更新密钥 2024-03-28 23:01:52 +08:00
liuweiqing
9e2510d748 chore: docs 2024-03-27 12:08:07 +08:00
liuweiqing
0a232f495a chore: 尝试修复Google登录 2024-03-15 16:17:45 +08:00
liuweiqing
5ccdc5270a chore: 尝试修复Google登录 2024-03-15 16:05:18 +08:00
liuweiqing
6bf8061f0f feat: 优化未找到文献的提示 2024-03-15 15:47:56 +08:00
liuweiqing
5d61d49f05
Merge pull request #28 from 14790897/release-v1.9.0
chore: release 1.9.0
2024-03-14 16:56:39 +08:00
liuweiqing
18356b67d9 chore: release 1.9.0 2024-03-13 21:22:58 +08:00
14790897
0bfaacf6ee fix: remove .env.local 2024-03-13 21:22:31 +08:00
14790897
6243aa5401 fix: 前端环境变量需要使用NEXT_PUBLIC_ 2024-03-13 21:07:10 +08:00
14790897
64cab48ae3 fix: 应用id问题 2024-03-13 20:55:50 +08:00
14790897
0090ffd3bb feat: 完成linuxdo oauth 2024-03-13 20:51:58 +08:00
liuweiqing
8730415352 fix: 通过判断user是否登陆来决定是否one tap 2024-03-09 23:13:12 +08:00
liuweiqing
2f60e65f91 chore: 使用同步尝试去除onetap 2024-03-09 23:01:03 +08:00
liuweiqing
c8f3a94520 chore: 使用同步尝试去除onetap 2024-03-09 22:58:23 +08:00
liuweiqing
fdcadbec60 chore: 使用同步尝试去除onetap 2024-03-09 22:40:06 +08:00
liuweiqing
e0426cf5fc chore: 使用同步尝试去除onetap 2024-03-09 21:55:25 +08:00
liuweiqing
b5bd878cda fix: 点击空白页面可以可以取消论文列表页 2024-03-09 19:50:08 +08:00
liuweiqing
7f5af058cd chore: 重设密码时密码要输入两次 2024-03-09 19:07:46 +08:00
liuweiqing
939f5c28e9 feat: 允许重置密码 2024-03-09 18:51:38 +08:00
liuweiqing
f63dd8865b chore: 重置密码 2024-03-09 16:15:49 +08:00
liuweiqing
5f3252da6e fix: 谷歌登陆后不再弹出 2024-03-09 16:10:55 +08:00
14790897
d808aa3195 chore: 密码重置界面 2024-03-08 19:56:57 +08:00
14790897
84239677f5 chore: google onetap 2024-03-08 12:00:36 +08:00
14790897
04a3e64e64 chore: google onetap 2024-03-08 11:45:43 +08:00
14790897
2267c98d7a chore: google onetap 2024-03-08 11:41:54 +08:00
14790897
c91006564b google onetap 2024-03-08 11:16:26 +08:00
14790897
7243d8bc84 chore: google onetap 2024-03-08 11:08:20 +08:00
14790897
3446ce4ced chore: Google onetap 2024-03-08 10:49:45 +08:00
14790897
3fd86d788b chore: 尝试修复oauth 2024-03-07 21:55:19 +08:00
14790897
dfca412ca6 chore: google登录尝试修复 2024-03-07 18:41:55 +08:00
14790897
dfab6991e0 chore: 调试 2024-03-07 16:11:38 +08:00
14790897
15c3a1f0ac feat: google登录 2024-03-07 13:46:08 +08:00
14790897
a0ce164b15 chore: 服务条款 2024-03-07 11:45:25 +08:00
14790897
6315d48d89 chore: privacy 2024-03-07 11:38:10 +08:00
14790897
f58ce4c7c4 chore: remove nodes 2024-03-07 10:54:38 +08:00
14790897
a0e88d8c8d chore: profile表加上时间列 2024-03-07 10:47:55 +08:00
14790897
559b4010c2 fix: GitHub登入可以插入信息 2024-03-07 10:08:17 +08:00
14790897
17ce170ab3 chore: 尝试修复GitHub登录 2024-03-06 22:53:16 +08:00
14790897
c037ac1db5 chore: 尝试修复GitHub登录 2024-03-06 21:41:44 +08:00
14790897
4b64827c1d fix: 新用户没有获得编辑器焦点会导致报错
if (useEditorFlag && editor && cursorPosition !== null) {
2024-03-06 14:06:11 +08:00
14790897
f8e4cfd205 fix: remove freshwork 2024-03-06 13:04:43 +08:00
14790897
836aa49847 fix: 使用cf反代解决semantic cors问题 2024-03-06 11:33:54 +08:00
14790897
151a1aa286 chore: 加了两个模型 2024-03-06 11:02:04 +08:00
14790897
d2263c503f chore: api更新 2024-03-04 15:43:38 +08:00
14790897
a4b368c5c6 chore: 加个gemini 2024-03-01 15:04:08 +08:00
14790897
c23f83a439 chore: 优化掉不用的包 2024-02-29 22:32:42 +08:00
14790897
35dbdcc2b2 chore: api update 2024-02-29 15:54:24 +08:00
14790897
1cf4c37295 chore: 取消公告 2024-02-29 14:26:29 +08:00
14790897
94f960363c chore: api更新 2024-02-29 14:25:33 +08:00
14790897
186a6750c2 chore: api 2024-02-28 20:04:26 +08:00
14790897
103523ee52 chore: api去除蒙恬 2024-02-28 17:06:30 +08:00
14790897
79cc718951 chore: api描述 2024-02-28 15:24:29 +08:00
liuweiqing
624715afd5 chore: semantic不可用 2024-02-27 22:46:22 +08:00
liuweiqing
d5fb68ecbe chore: remove code 2024-02-27 22:45:40 +08:00
liuweiqing
a31dc819a9 chore: docker-compose部署方式 2024-02-27 17:07:35 +08:00
liuweiqing
efabb39dfc refractor: 主要的和ai交互的函数重构 2024-02-27 16:29:01 +08:00
liuweiqing
4c78494e4d chore: msft clarify 2024-02-27 13:38:39 +08:00
liuweiqing
37fa37e9ac ci: fork仓库自动更新 2024-02-27 11:28:01 +08:00
liuweiqing
b8f613525a chore: announcement 2024-02-26 23:49:46 +08:00
14790897
e7aa998ca7 fix: 读取ai响应 2024-02-26 20:33:52 +08:00
14790897
43f222a83e chore: coze api 2024-02-26 20:17:16 +08:00
liuweiqing
a72329d4a2 feat: 可以手动停止AI的输出(左下角按钮) 2024-02-26 09:57:42 +08:00
liuweiqing
1eb3c596f3 chore: api 2024-02-26 09:42:08 +08:00
liuweiqing
65db119a9a fix: github登录无法将用户数据插入数据库 2024-02-25 21:21:21 +08:00
liuweiqing
478d228313 chore: api更新 2024-02-25 15:17:33 +08:00
liuweiqing
f8335cb03b style: 通告和加载提示 2024-02-25 14:21:32 +08:00
liuweiqing
b0b8707f05 chore: api 2024-02-25 11:30:08 +08:00
liuweiqing
8a25248c29 fix: quilleditor ssr加载失败 2024-02-25 10:27:33 +08:00
liuweiqing
a3a499a438 chore: toast 2024-02-24 23:04:12 +08:00
liuweiqing
72300bf6eb fix: seo 2024-02-24 21:27:24 +08:00
liuweiqing
e4ba9f09db Merge branch 'main' of https://github.com/14790897/paper-ai 2024-02-24 14:30:00 +08:00
liuweiqing
6e807a703d fix: seo图片问题 2024-02-24 14:29:54 +08:00
liuweiqing
2a7867963e
Merge pull request #26 from 14790897/release-v1.8.0
chore: release 1.8.0
2024-02-24 13:49:18 +08:00
liuweiqing
49f396f961 chore: 调整通知时间 2024-02-24 13:47:34 +08:00
liuweiqing
0ffb6e1324 chore: release 1.8.0 2024-02-24 12:08:12 +08:00
liuweiqing
6c37459fe3 feat: 论文搜索完成进行提示 2024-02-24 12:07:55 +08:00
liuweiqing
93f8889c57 feat: 优雅的报错提示 2024-02-24 11:20:15 +08:00
liuweiqing
f5bdd4f13d chore: update api 2024-02-24 09:58:56 +08:00
liuweiqing
4dc951c211 docs: readme 2024-02-23 21:23:31 +08:00
liuweiqing
491a374255 chore: 用户友好的提示 2024-02-23 21:07:59 +08:00
liuweiqing
49a757f6c7 feat: 增加了时间范围选择,除arxiv 2024-02-23 20:59:29 +08:00
liuweiqing
a38b9cee52 fix: 复制文献问题 2024-02-23 19:52:07 +08:00
liuweiqing
9b835bbadd feat: 可以切换多种引用格式 2024-02-23 19:17:12 +08:00
liuweiqing
cbc1e7068b Merge branch 'main' of https://github.com/14790897/paper-ai 2024-02-23 12:58:40 +08:00
liuweiqing
1ce20dc031 chore: dependency 2024-02-23 12:58:35 +08:00
liuweiqing
0926b24530 chore: 更新api 2024-02-23 12:35:27 +08:00
liuweiqing
6270f427c3
Merge pull request #24 from 14790897/release-v1.7.0
chore: release 1.7.0
2024-02-22 13:17:17 +08:00
liuweiqing
6f11a809f8
Merge pull request #23 from 14790897/dependabot/npm_and_yarn/npm_and_yarn-security-group-8ad25f4c4b
chore(deps): bump the npm_and_yarn group across 2 directories with 2 updates
2024-02-22 13:16:30 +08:00
liuweiqing
baaf5a33c1
Merge branch 'main' into dependabot/npm_and_yarn/npm_and_yarn-security-group-8ad25f4c4b 2024-02-22 13:16:18 +08:00
liuweiqing
28d63ea406 refractor: 使得判断文献是否修改更有效率 2024-02-22 12:35:08 +08:00
liuweiqing
8b725144c2 chore: release 1.7.0 2024-02-22 08:35:19 +08:00
liuweiqing
228f1672f4 chore: 蒙恬的免费api(300$) 2024-02-22 08:34:54 +08:00
liuweiqing
1d15248c5e chore: 更新caifree 2024-02-21 22:03:51 +08:00
liuweiqing
4b7f847d72 chore: 取消service worker 2024-02-21 21:23:21 +08:00
liuweiqing
3004ae2a57 chore: 尝试react-quill,但没有解决报错 2024-02-21 20:35:22 +08:00
liuweiqing
5d80cab0af style: css 2024-02-21 16:52:05 +08:00
liuweiqing
fcd174a952 style: css 2024-02-21 15:50:43 +08:00
liuweiqing
ff09eee993 feat: pwa可离线访问 service worker 2024-02-21 15:04:02 +08:00
liuweiqing
b92e425517 chore: 更新caifree令牌 2024-02-20 23:33:08 +08:00
liuweiqing
81552993be fix: 用户反馈组件 2024-02-20 22:26:53 +08:00
liuweiqing
cef12f31a0 feat: 加了个用户反馈组件 2024-02-20 22:11:02 +08:00
liuweiqing
0d327febea chore: 图标优化 2024-02-20 15:26:54 +08:00
liuweiqing
bedc8a3ce0 feat: seo优化 2024-02-20 15:00:22 +08:00
liuweiqing
6cda6d176a feat: 可选的对文献相关性检验 2024-02-20 10:52:33 +08:00
liuweiqing
b55cf4929a chore: 移动文件 2024-02-20 09:59:48 +08:00
liuweiqing
f05e6c3577 chore: 一个新的API 2024-02-20 09:59:11 +08:00
liuweiqing
70784c7738 chore: sentry 2024-02-19 20:32:02 +08:00
liuweiqing
53774a34a7
Merge pull request #20 from 14790897/release-v1.6.0
chore: release 1.6.0
2024-02-19 16:58:13 +08:00
liuweiqing
525619bce9 chore: release 1.6.0 2024-02-19 16:57:48 +08:00
liuweiqing
83aab38891 ci: sentry 2024-02-19 16:57:29 +08:00
liuweiqing
a707ead9cc chore: package 2024-02-18 23:46:18 +08:00
liuweiqing
f2b6bde907 chore: papermanage 2024-02-18 22:36:31 +08:00
liuweiqing
8ab1bdd393 fix: 图标正常显示 2024-02-18 22:36:03 +08:00
liuweiqing
3040c11ea1 fix: 只有在AI返回的内容没有错之后才能添加文献 2024-02-18 21:20:35 +08:00
liuweiqing
c55a93c79b feat: 增加了一个显示当前任务进度的进度条 2024-02-18 20:41:36 +08:00
liuweiqing
e5ee52ae2d chore: env.local上传 2024-02-18 20:06:59 +08:00
liuweiqing
96d780dd3a fix: ai对话 2024-02-18 15:59:46 +08:00
liuweiqing
763a1062f9 feat: 如果AI多次引用同一文献则只返回第一个文献的引用数字 2024-02-18 15:48:18 +08:00
liuweiqing
07070bf253 fix: pubmed参数写反 2024-02-18 14:54:31 +08:00
liuweiqing
9d799f1736 feat: 可以进行多轮文献查询 2024-02-18 12:10:53 +08:00
liuweiqing
edfb1c5475 docs: 文档添加 2024-02-18 09:52:32 +08:00
liuweiqing
70e9011361 docs: readme 2024-02-17 19:54:51 +08:00
liuweiqing
72b4ff267f Revert "Update .gitignore"
This reverts commit ef48a77058.
2024-02-17 14:26:04 +08:00
liuweiqing
ef48a77058 Update .gitignore 2024-02-17 14:25:52 +08:00
liuweiqing
0db233ff02 ci: docs 2024-02-17 13:04:40 +08:00
liuweiqing
1ab5e307b2 ci: docs 2024-02-17 12:59:57 +08:00
liuweiqing
29deadd76e ci: docs 2024-02-17 12:56:03 +08:00
liuweiqing
11b700e81f ci: remobe node 2024-02-17 12:41:06 +08:00
liuweiqing
c1c34b340c ci: docs 2024-02-17 12:13:34 +08:00
liuweiqing
81a5f6c286 ci: docs 2024-02-17 12:10:03 +08:00
liuweiqing
e0728b7b9a ci: docs 2024-02-17 12:04:56 +08:00
liuweiqing
4856f001e1 ci: docs 2024-02-17 12:01:35 +08:00
liuweiqing
c4d1ad61c2 Merge branch 'main' of https://github.com/14790897/paper-ai 2024-02-17 12:01:26 +08:00
liuweiqing
7ff2b8669b ci: docs 2024-02-17 12:01:24 +08:00
liuweiqing
c5939fee3b
ci: docs 2024-02-17 11:34:52 +08:00
liuweiqing
5c99a1e6ad docs: 文档网站 2024-02-17 11:31:41 +08:00
liuweiqing
4c52a20524 docs: 文档网站 2024-02-17 11:27:35 +08:00
liuweiqing
762601f308 ci: docs 2024-02-17 11:24:10 +08:00
liuweiqing
2bf3ea29ee docs: 文档网站 2024-02-17 11:18:01 +08:00
dependabot[bot]
9b27ba0595
chore(deps): bump the npm_and_yarn group across 2 directories with 2 updates
Bumps the npm_and_yarn group with 2 updates in the /. directory: [postcss](https://github.com/postcss/postcss) and [sweetalert2](https://github.com/sweetalert2/sweetalert2).


Updates `postcss` from 8.4.31 to 8.4.35
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.4.31...8.4.35)

Updates `sweetalert2` from 11.10.4 to 11.10.5
- [Release notes](https://github.com/sweetalert2/sweetalert2/releases)
- [Changelog](https://github.com/sweetalert2/sweetalert2/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sweetalert2/sweetalert2/compare/v11.10.4...v11.10.5)

---
updated-dependencies:
- dependency-name: postcss
  dependency-type: direct:production
  dependency-group: npm_and_yarn-security-group
- dependency-name: sweetalert2
  dependency-type: direct:production
  dependency-group: npm_and_yarn-security-group
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-17 03:05:21 +00:00
liuweiqing
61576aaa6d docs: 文档网站 2024-02-17 11:03:55 +08:00
liuweiqing
f9dcc30103 docs: 文档网站 2024-02-17 10:38:30 +08:00
liuweiqing
06f57ab3ec refractor: 将格式化文献的函数重构到公共函数中 2024-02-15 20:13:26 +08:00
liuweiqing
5e7afea400 fix: 修复lemon路由 2024-02-15 19:57:29 +08:00
liuweiqing
01d3ffebf7 fix: 导出word时文献不更新,因为usecallback没有添加变量 2024-02-15 18:30:44 +08:00
liuweiqing
da5cb4fac8 fix: 去除暗色模式 2024-02-15 16:58:36 +08:00
liuweiqing
4ee08169df feat: github登录(测试中) 2024-02-14 23:16:03 +08:00
liuweiqing
c03741c396 chore: email template 2024-02-14 21:37:52 +08:00
liuweiqing
03febb1364 fix: translation 2024-02-14 16:54:54 +08:00
liuweiqing
dfbbf17580 chore: delete 2024-02-13 19:58:43 +08:00
liuweiqing
b603365265 fix: 论文引用格式 2024-02-13 19:54:32 +08:00
liuweiqing
c1f201b033 Merge branch 'main' of https://github.com/14790897/paper-ai 2024-02-13 16:35:37 +08:00
liuweiqing
5374e7a1d3 chore: readme 💯 2024-02-13 16:35:34 +08:00
liuweiqing
a9d7a87c22
Merge pull request #18 from 14790897/release-v1.5.0
chore: release 1.5.0
2024-02-13 16:33:06 +08:00
liuweiqing
2d6f728d7c chore: release 1.5.0 2024-02-13 16:32:21 +08:00
liuweiqing
e5d267cf70 feat: 加了一个按钮用来控制鼠标点击段落中的上标跳转到文献引用 2024-02-13 16:32:04 +08:00
liuweiqing
3636fe1c35 feat: pubmed论文格式更新 2024-02-13 16:01:16 +08:00
liuweiqing
089a7afa7e fix: 删除段落的错误 2024-02-13 15:30:12 +08:00
liuweiqing
c4d410073c feat: 更准确的文献引用 2024-02-13 15:12:48 +08:00
liuweiqing
9e85552d89 feat: 记录selection位置以及自动清空UserInput内容 2024-02-13 14:13:40 +08:00
liuweiqing
e3a874780e Merge branch 'main' of https://github.com/14790897/paper-ai 2024-02-13 13:15:43 +08:00
liuweiqing
342d78bd88 fix: 未选中焦点的时候会报错 2024-02-13 13:15:38 +08:00
liuweiqing
c223b15b48
Merge pull request #16 from 14790897/release-v1.4.0
chore: release 1.4.0
2024-02-12 22:56:20 +08:00
liuweiqing
c202dc07d3 chore: release 1.4.0 2024-02-12 22:55:17 +08:00
liuweiqing
67212d6a85 fix: reference中的journal终于搞定 2024-02-12 22:54:53 +08:00
liuweiqing
2573950ad8 feat: i18n 切换 2024-02-12 21:35:00 +08:00
liuweiqing
d64295e27a feat: i18n中文英文 2024-02-12 20:55:14 +08:00
liuweiqing
38ede8e285 refractor: quillwrapper 2024-02-12 15:00:21 +08:00
liuweiqing
bab4e2b90e chore: sentry,i18n,buyvip 2024-02-12 13:28:16 +08:00
liuweiqing
b983427721 feat: sentry追踪用户 2024-02-11 23:11:34 +08:00
liuweiqing
f5ee32669c feat: sentry 2024-02-11 22:49:17 +08:00
liuweiqing
acd1541311 chore: GA 2024-02-11 22:19:24 +08:00
liuweiqing
68b02693b0 docs: dokcer 2024-02-11 19:36:43 +08:00
liuweiqing
8c7affc541 ci: dockerhub 2024-02-11 18:41:40 +08:00
liuweiqing
8f2dde43be ci: docker 2024-02-11 18:40:21 +08:00
liuweiqing
6a569e1d03 ci: dockerfile 2024-02-11 18:37:02 +08:00
liuweiqing
cb2c46c7ca ci: dockerhub 2024-02-11 18:29:38 +08:00
liuweiqing
336399e76b ci: docker尝试 2024-02-11 14:28:03 +08:00
liuweiqing
a798f26606 style: textarea大小 2024-02-11 12:22:27 +08:00
liuweiqing
c9f1791e70 style: 交换添加文献和文献内容区域的顺序 2024-02-11 12:21:15 +08:00
liuweiqing
0a2f4316bf docs: github 2024-02-11 11:50:04 +08:00
liuweiqing
e787841193 style: 设置icon样式 2024-02-11 11:34:57 +08:00
liuweiqing
b9cf0c9cb2 Merge branch 'main' of https://github.com/14790897/paper-ai 2024-02-10 23:12:39 +08:00
liuweiqing
ed60717b3d chore: 彩蛋 2024-02-10 23:12:34 +08:00
liuweiqing
8e7fcafe46
Merge pull request #10 from 14790897/release-v1.3.0
chore: release 1.3.0
2024-02-10 22:36:39 +08:00
liuweiqing
acad00b391 chore: release 1.3.0 2024-02-10 21:53:23 +08:00
liuweiqing
165f189efa fix: 将输入栏变得方便 2024-02-10 21:53:05 +08:00
liuweiqing
7fd9032f26 style: 适配手机 2024-02-10 17:35:51 +08:00
liuweiqing
425285bb9f docs: remove 2024-02-10 13:55:09 +08:00
liuweiqing
c3b0a940ed chore: .. 2024-02-10 13:47:55 +08:00
liuweiqing
74486f95c2 fix: 刷新之后不获取云端文献引用 2024-02-10 13:26:03 +08:00
liuweiqing
4e6f628289 fix: 文献引用删除修复 2024-02-10 13:25:43 +08:00
liuweiqing
8629d20341 feat: 注册时在profiles插入用户信息,方便设置vip时读取email操作 2024-02-10 13:12:45 +08:00
liuweiqing
6e6cf16fba fix: lemon 2024-02-10 00:55:16 +08:00
liuweiqing
f1f993189c chore: 调试lemon 2024-02-10 00:50:20 +08:00
liuweiqing
d6a86e12dd Merge branch 'main' of https://github.com/14790897/paper-ai 2024-02-10 00:45:16 +08:00
liuweiqing
acae014a46 fix: lemon 2024-02-10 00:44:49 +08:00
liuweiqing
840da347e5
Merge pull request #14 from 14790897/supa
fix: vercel部署密钥问题
2024-02-09 23:14:54 +08:00
liuweiqing
84e0363313 fix: vercel部署密钥问题 2024-02-09 23:09:48 +08:00
liuweiqing
729de74653
Merge pull request #13 from 14790897/supa
Supa and Lemon
2024-02-09 23:03:02 +08:00
liuweiqing
01d703a17b fix: 引用改变的时候立刻同步 2024-02-09 23:01:05 +08:00
liuweiqing
0276ff8964 feat: 引用和内容分开更新 2024-02-09 23:01:01 +08:00
liuweiqing
2286a48fc0 fix: 解决向云端同步的时候文章内容不更新 2024-02-09 20:26:13 +08:00
liuweiqing
486c75d4d7 fix: 解决因为闭包导致paperNumberRedux 不正确的问题 2024-02-09 18:01:28 +08:00
liuweiqing
833e2e1b0e fix: 解决vip状态不正确,因为没有使用dispatch 2024-02-09 18:01:00 +08:00
liuweiqing
91ab703708 feat: 完善了错误处理
可以从用户界面看到报错原因
2024-02-09 13:45:26 +08:00
liuweiqing
a72342504f feat: 在设置界面保存了多个可用的配置 2024-02-09 11:51:29 +08:00
liuweiqing
02357cc036 fix: system prompt可以输入 2024-02-08 23:28:33 +08:00
liuweiqing
91e9759cb7 fix: isVip改为redux状态 2024-02-08 23:06:35 +08:00
liuweiqing
481ffd0697 chore: others 2024-02-08 21:57:21 +08:00
liuweiqing
2061f6c36a style: gold color 2024-02-08 15:47:49 +08:00
liuweiqing
6b9dce875d chore: sql备份 2024-02-08 15:47:37 +08:00
liuweiqing
aeff96032e feat: vip按钮 2024-02-08 15:47:25 +08:00
liuweiqing
9cb214d67f fix: 修复service-role无法访问 2024-02-08 14:14:50 +08:00
liuweiqing
d0e6a72f0d fix: 修复service-role无法访问 2024-02-08 14:13:53 +08:00
liuweiqing
4fa779698e fix: redux 2024-02-08 10:31:29 +08:00
liuweiqing
6e8b3049f6 style: 管理云端文章的按钮 2024-02-08 10:30:58 +08:00
liuweiqing
db775174e0 style: 管理云端文章的按钮 2024-02-08 10:30:33 +08:00
liuweiqing
2ca6d3d212 feat: 编辑过程中同步云端 2024-02-08 10:30:03 +08:00
liuweiqing
e7567cb49d feat: supa utils 2024-02-08 10:29:42 +08:00
liuweiqing
2be00c56d3 chore: other 2024-02-08 10:29:21 +08:00
liuweiqing
fe31198124 feat: 管理云端多篇论文功能 2024-02-08 10:28:52 +08:00
liuweiqing
20add6b617 feat: supa路由 2024-02-08 10:28:00 +08:00
liuweiqing
9ee43ebd06 fix: settings的redux 2024-02-07 16:13:36 +08:00
liuweiqing
a3d1cebd09 chore: other code 2024-02-07 16:13:20 +08:00
liuweiqing
1d4fbaf8e4 fix: settings的redux 2024-02-07 16:12:40 +08:00
liuweiqing
783b8d343e chore: remove billing 2024-02-07 15:53:36 +08:00
liuweiqing
be7c3781de refract: settings页面 2024-02-05 23:14:40 +08:00
liuweiqing
fbd899cae3 feat: lemonsqueezy1 2024-02-05 23:11:47 +08:00
liuweiqing
c6c9914e84 chore: 代码移除 2024-02-05 23:10:59 +08:00
liuweiqing
d6b3f988c5 chore: 代码移除 2024-02-05 23:10:16 +08:00
liuweiqing
4a15484c36 chore: 代码移除 2024-02-05 23:08:44 +08:00
liuweiqing
1af98048a5 chore: 代码移除 2024-02-05 23:08:01 +08:00
liuweiqing
10e7ef05c9 feat: 使用免费的deepseek 2024-02-04 12:55:09 +08:00
liuweiqing
8dcb54a49b feat: 导出word功能增加导出文献内容 2024-02-04 11:02:10 +08:00
liuweiqing
41e3e7c86f style: 设置界面 2024-02-03 23:07:02 +08:00
liuweiqing
3580c34e83 feat: 点击引用数字调转到对应文献 2024-02-03 17:30:46 +08:00
liuweiqing
096a06178c chore: 无用的代码移除 2024-02-03 16:00:59 +08:00
liuweiqing
f761c357ea fix: 撤销 2024-02-03 14:06:37 +08:00
liuweiqing
88063baa2e fix: AI输入的时候鼠标可以失去焦点 2024-02-03 14:06:26 +08:00
liuweiqing
65da583258 feat: 删除引用的时候同时删除段落 2024-02-03 14:06:01 +08:00
liuweiqing
732dd738c9 feat: 尝试删除索引的时候删除整个段落 2024-02-02 22:59:21 +08:00
liuweiqing
d56f427484 fix: 插入前先换行 2024-01-31 22:52:37 +08:00
liuweiqing
27c0ffed93 chore: api 2024-01-30 15:16:39 +08:00
liuweiqing
6107267123 fix: 自动保存编辑器内容 2024-01-30 12:05:34 +08:00
liuweiqing
ba8722afda feat: 自动识别最近的文献序号在那之后插入新的文献 2024-01-30 09:00:16 +08:00
liuweiqing
3c54b8c116 chore: other 2024-01-29 22:47:17 +08:00
liuweiqing
c0515cfc8c Merge branch 'main' of https://github.com/14790897/paper-ai 2024-01-29 22:46:54 +08:00
liuweiqing
f5ae3c1ff4 fix: 修复pubmed articleUrl 无法正常获取的问题 2024-01-29 20:37:40 +08:00
liuweiqing
965049695e
Merge pull request #8 from 14790897/release-v1.2.0
chore: release 1.2.0
2024-01-29 17:51:18 +08:00
liuweiqing
6ca5ebcc4c chore: readme 2024-01-29 17:50:41 +08:00
liuweiqing
7564d56c84 chore: readme 2024-01-29 17:48:47 +08:00
liuweiqing
9fce47862f style: export样式 2024-01-29 17:47:43 +08:00
liuweiqing
b017d03aa5 chore: release 1.2.0 2024-01-29 17:45:29 +08:00
liuweiqing
cc2856ceb2 feat: 导出到word功能,避免样式丢失 2024-01-29 17:45:13 +08:00
liuweiqing
c7cef370d0 feat: 上下移动文献功能 2024-01-29 15:01:43 +08:00
liuweiqing
52676f3bb1 Merge branch 'main' of https://github.com/14790897/paper-ai 2024-01-29 13:35:28 +08:00
liuweiqing
c160d3e6af feat: 系统提示自定义 2024-01-29 13:35:24 +08:00
111 changed files with 8753 additions and 1307 deletions

18
.env.local Normal file
View File

@ -0,0 +1,18 @@
# Update these with your Supabase details from your project settings > API
# https://app.supabase.com/project/_/settings/api
NEXT_PUBLIC_SUPABASE_URL=https://yidfukfbrluizjvfrrsj.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InlpZGZ1a2Zicmx1aXpqdmZycnNqIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MDQ4NjMyNjEsImV4cCI6MjAyMDQzOTI2MX0.EXIXAdNIGLFo5wHmwmY2-9bqLO9vyFYDvMMtCtkxgig
# NEXT_PUBLIC_TINYMCE_API_KEY=e983nu390inks6be1wwlsrdxjebot3yc4pld7d44zs6vcrxr
NEXT_PUBLIC_OPENAI_API_KEY=sk-ffe19ebe9fa44d00884330ff1c18cf82
#sess-wZKsUKS8IhPH3jI44krQL41vhl68qH6hwE66hOnL
NEXT_PUBLIC_SEMANTIC_API_KEY=hEQvK6ARe84dzDPcMnpzX4n9jfoqztkMfaftPWnb
NEXT_PUBLIC_PUBMED_API_KEY=057616e7ce6c722f2ae8679e38a8be9b1a09
NEXT_PUBLIC_AI_URL=https://api.deepseek.com # /api/v1/chat/completions
NEXT_PUBLIC_PAPER_URL=/api/paper
#"https://api.openai.com/v1/chat/completions" "https://api.liuweiqing.top" "https://api.liuweiqing.top/v1/chat/completions"
#node转发设置为 /api/v1/chat/completions https://one.caifree.com sk-aiHrrRLYUUelHstX69E9484509254dBf92061d6744FfFaD1
VERCEL_URL=https://www.paperai.life
NODE_ENV=development
# NEXT_PUBLIC_CLIENT_ID=UrgIEI0n03tveTmaOV0IU8qRY4DttGY4
# CLIENT_SECRET=ljShbIlIrfULu4BTUVTT4azeR90PtAif
# REDIRECT_URI=http://localhost:3000/api/oauth/callback

View File

@ -1,7 +1,12 @@
NEXT_PUBLIC_SUPABASE_URL=https://yidfukfbrluizjvfrrsj.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InlpZGZ1a2Zicmx1aXpqdmZycnNqIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MDQ4NjMyNjEsImV4cCI6MjAyMDQzOTI2MX0.EXIXAdNIGLFo5wHmwmY2-9bqLO9vyFYDvMMtCtkxgig
NEXT_PUBLIC_AI_URL=https://one.caifree.com
NEXT_PUBLIC_OPENAI_API_KEY=sk-aiHrrRLYUUelHstX69E9484509254dBf92061d6744FfFaD1
NEXT_PUBLIC_AI_URL=https://api.deepseek.com
NEXT_PUBLIC_OPENAI_API_KEY=sk-ffe19ebe9fa44d00884330ff1c18cf82
NEXT_PUBLIC_PAPER_URL=/api/paper
NEXT_PUBLIC_SEMANTIC_API_KEY=hEQvK6ARe84dzDPcMnpzX4n9jfoqztkMfaftPWnb
NEXT_PUBLIC_PUBMED_API_KEY=057616e7ce6c722f2ae8679e38a8be9b1a09
VERCEL_URL=https://www.paperai.life
NODE_ENV=production
NEXT_PUBLIC_CLIENT_ID=RcgInz3KqEhb2KdW2yg5WUgAf3KHcJAC
CLIENT_SECRET=U4z8TgPIV1GWCXhFFNEVQyfmDotf91K6
REDIRECT_URI=https://www.paperai.life/api/oauth/callback

28
.github/workflows/docker.yml vendored Normal file
View File

@ -0,0 +1,28 @@
name: PaperAI Docker Image Push
on:
push:
branches:
- main
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Check Out Repo
uses: actions/checkout@v2
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Build and Push Image
uses: docker/build-push-action@v2
with:
context: ./
file: ./Dockerfile
push: true
tags: 14790897/paperai:latest

40
.github/workflows/sync.yml vendored Normal file
View File

@ -0,0 +1,40 @@
name: Upstream Sync
permissions:
contents: write
on:
schedule:
- cron: "0 0 * * *" # every day
workflow_dispatch:
jobs:
sync_latest_from_upstream:
name: Sync latest commits from upstream repo
runs-on: ubuntu-latest
if: ${{ github.event.repository.fork }}
steps:
# Step 1: run a standard checkout action
- name: Checkout target repo
uses: actions/checkout@v3
# Step 2: run the sync action
- name: Sync upstream changes
id: sync
uses: aormsby/Fork-Sync-With-Upstream-action@v3.4
with:
upstream_sync_repo: 14790897/paper-ai
upstream_sync_branch: main
target_sync_branch: main
target_repo_token: ${{ secrets.GITHUB_TOKEN }} # automatically generated, no need to set
# Set test_mode true to run tests instead of the true action!!
test_mode: false
- name: Sync check
if: failure()
run: |
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 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

20
.gitignore vendored
View File

@ -26,7 +26,7 @@ yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# .env*.local
# vercel
.vercel
@ -35,4 +35,20 @@ yarn-error.log*
*.tsbuildinfo
next-env.d.ts
pass
pass
.env
# .env.local
.vercel
post.md
# Sentry Config File
.sentryclirc
#vitepress
docs/.vitepress/dist/*
node_modules
dist
cache
# Sentry Config File
.sentryclirc

83
.vscode/settings.json vendored
View File

@ -1,19 +1,68 @@
{
"sqltools.connections": [
{
"mysqlOptions": {
"authProtocol": "default",
"enableSsl": "Disabled"
},
"previewLimit": 50,
"server": "localhost",
"port": 3306,
"driver": "MySQL",
"name": "local",
"database": "b3log_symphony",
"username": "root",
"password": "123456"
}
"sqltools.connections": [
{
"mysqlOptions": {
"authProtocol": "default",
"enableSsl": "Disabled"
},
"previewLimit": 50,
"server": "localhost",
"port": 3306,
"driver": "MySQL",
"name": "local",
"database": "b3log_symphony",
"username": "root",
"password": "123456"
}
],
"commentTranslate.targetLanguage": "zh-CN",
"i18n-ally.localesPaths": ["../../../git-program/paper-ai/app/i18n"],
"i18n-ally.keystyle": "nested",
"i18n-ally.namespace": true,
"i18n-ally.enabledParsers": ["json", "js"],
"i18n-ally.sortKeys": true,
"i18n-ally.sourceLanguage": "zh-CN",
"i18n-ally.displayLanguage": "en",
"i18n-ally.editor.preferEditor": true,
"i18n-ally.extract.keygenStyle": "camelCase",
"i18n-ally.enabledFrameworks": ["i18next"],
// Parser options for extracting HTML, see https://github.com/lokalise/i18n-ally/blob/master/src/extraction/parsers/options.ts
"i18n-ally.extract.parsers.html": {
"attributes": [
"text",
"title",
"alt",
"placeholder",
"label",
"aria-label",
"button"
],
"commentTranslate.targetLanguage": "zh-CN"
}
"ignoredTags": ["script", "style"],
"vBind": true,
"inlineText": true
},
// Enables hard-coded strings detection automatically whenever opening supported a file
"i18n-ally.extract.autoDetect": true,
// Make sure that particular refactoring templates would be picked up be the bulk extraction depending on the context
"i18n-ally.refactor.templates": [
{
// affect scope (optional)
// see https://github.com/lokalise/i18n-ally/blob/master/src/core/types.ts#L156-L156
"source": "html-attribute",
"templates": ["i18n.t('{key}'{args})", "custom.t('{key}'{args})"],
// accept globs, resolved to project root (optional)
"include": ["app/**/*.{vue,ts,js,tsx}", "index.html"],
"exclude": ["src/config/**"]
}
// ...
]
}

View File

@ -1,5 +1,175 @@
# Changelog
## [1.9.0](https://www.github.com/14790897/paper-ai/compare/v1.8.0...v1.9.0) (2024-03-13)
### Features
* google登录 ([15c3a1f](https://www.github.com/14790897/paper-ai/commit/15c3a1f0acae8f5d6e610382b4fb886cc637d4f9))
* 允许重置密码 ([939f5c2](https://www.github.com/14790897/paper-ai/commit/939f5c28e9f658f2899cb262c16e78d42601a320))
* 可以手动停止AI的输出(左下角按钮) ([a72329d](https://www.github.com/14790897/paper-ai/commit/a72329d4a209aba4a0111093ece3b8cf89113ad2))
* 完成linuxdo oauth ([0090ffd](https://www.github.com/14790897/paper-ai/commit/0090ffd3bbe83a99a4de15e733619569cd78a69b))
### Bug Fixes
* GitHub登入可以插入信息 ([559b401](https://www.github.com/14790897/paper-ai/commit/559b4010c276f45dec791aaea17133d6690aac63))
* github登录无法将用户数据插入数据库 ([65db119](https://www.github.com/14790897/paper-ai/commit/65db119a9ac97009f15238e6ef26236bc6be14c7))
* quilleditor ssr加载失败 ([8a25248](https://www.github.com/14790897/paper-ai/commit/8a25248c29c578c75ec9e05fd1cdd339f131e5d6))
* remove .env.local ([0bfaacf](https://www.github.com/14790897/paper-ai/commit/0bfaacf6ee52309aaa55961037aba29a1558e6a6))
* remove freshwork ([f8e4cfd](https://www.github.com/14790897/paper-ai/commit/f8e4cfd205cfb5b5ec105f662c0ff0b6d8590429))
* seo ([72300bf](https://www.github.com/14790897/paper-ai/commit/72300bf6eb23757845734ccfcfb99d40274c9257))
* seo图片问题 ([6e807a7](https://www.github.com/14790897/paper-ai/commit/6e807a703d2a72f6ce557f043abefc44d746992c))
* 使用cf反代解决semantic cors问题 ([836aa49](https://www.github.com/14790897/paper-ai/commit/836aa49847ec7bc4fd2686a324a82ab19173fc83))
* 前端环境变量需要使用NEXT_PUBLIC_ ([6243aa5](https://www.github.com/14790897/paper-ai/commit/6243aa5401f7689aa7e4caf81a39a87578484299))
* 应用id问题 ([64cab48](https://www.github.com/14790897/paper-ai/commit/64cab48ae34ebb5500553dd3908167a95f1cbaf4))
* 新用户没有获得编辑器焦点会导致报错 ([4b64827](https://www.github.com/14790897/paper-ai/commit/4b64827c1d3c77dc05ebad359f0f4d384145211f))
* 点击空白页面可以可以取消论文列表页 ([b5bd878](https://www.github.com/14790897/paper-ai/commit/b5bd878cdafb6a54826f8aeafb16e6e6bc1e95bb))
* 读取ai响应 ([e7aa998](https://www.github.com/14790897/paper-ai/commit/e7aa998ca7dafe38a0c12ada4f168717c6e45439))
* 谷歌登陆后不再弹出 ([5f3252d](https://www.github.com/14790897/paper-ai/commit/5f3252da6e2715e4afbb7ab0b648112c22604230))
* 通过判断user是否登陆来决定是否one tap ([8730415](https://www.github.com/14790897/paper-ai/commit/87304153526200a2c3340c433c87348d5199287b))
## [1.8.0](https://www.github.com/14790897/paper-ai/compare/v1.7.0...v1.8.0) (2024-02-24)
### Features
* 优雅的报错提示 ([93f8889](https://www.github.com/14790897/paper-ai/commit/93f8889c5798b6f47dfb3a3831c051a11078786d))
* 可以切换多种引用格式 ([9b835bb](https://www.github.com/14790897/paper-ai/commit/9b835bbadd89763ab63fa159f35dd7ed657296c6))
* 增加了时间范围选择,除arxiv ([49a757f](https://www.github.com/14790897/paper-ai/commit/49a757f6c7d2a18bbde0e95cf57d4aaf5127e24c))
* 论文搜索完成进行提示 ([6c37459](https://www.github.com/14790897/paper-ai/commit/6c37459fe3b35f379e5b0b4d3c65224f32efb04d))
### Bug Fixes
* 复制文献问题 ([a38b9ce](https://www.github.com/14790897/paper-ai/commit/a38b9cee529a04100c4ebb8ba4129c2caa86f2d6))
## [1.7.0](https://www.github.com/14790897/paper-ai/compare/v1.6.0...v1.7.0) (2024-02-22)
### Features
* pwa可离线访问 service worker ([ff09eee](https://www.github.com/14790897/paper-ai/commit/ff09eee99343cfa6ee6da301428c4a0b3c790bf6))
* seo优化 ([bedc8a3](https://www.github.com/14790897/paper-ai/commit/bedc8a3ce0a314fd2cc7add65defdce372a1c432))
* 加了个用户反馈组件 ([cef12f3](https://www.github.com/14790897/paper-ai/commit/cef12f31a026be9ccc6229943f6a64b3ebf930db))
* 可选的对文献相关性检验 ([6cda6d1](https://www.github.com/14790897/paper-ai/commit/6cda6d176aac29b3972dfde305a62c0be0dc2437))
### Bug Fixes
* 用户反馈组件 ([8155299](https://www.github.com/14790897/paper-ai/commit/81552993bed272a5a05b19edd41aeb10106124b8))
## [1.6.0](https://www.github.com/14790897/paper-ai/compare/v1.5.0...v1.6.0) (2024-02-19)
### Features
* github登录测试中 ([4ee0816](https://www.github.com/14790897/paper-ai/commit/4ee08169df05a9bcf487a25c4b56c3785edbea7a))
* 可以进行多轮文献查询 ([9d799f1](https://www.github.com/14790897/paper-ai/commit/9d799f1736a7ff72e09f9e36e916c0ab9e04cec4))
* 增加了一个显示当前任务进度的进度条 ([c55a93c](https://www.github.com/14790897/paper-ai/commit/c55a93c79b20c7fb4b5ff15027110267d6874c24))
* 如果AI多次引用同一文献则只返回第一个文献的引用数字 ([763a106](https://www.github.com/14790897/paper-ai/commit/763a1062f982b5f0b96da5afb3b3bb96fd66eaef))
### Bug Fixes
* ai对话 ([96d780d](https://www.github.com/14790897/paper-ai/commit/96d780dd3a1c04e4c4adbd61cd87d07a42fa3eaa))
* pubmed参数写反 ([07070bf](https://www.github.com/14790897/paper-ai/commit/07070bf253044958931e138b5af0099ddc3fb8dd))
* translation ([03febb1](https://www.github.com/14790897/paper-ai/commit/03febb136415fcd48a4248d19c1086bfc0c95f8d))
* 修复lemon路由 ([5e7afea](https://www.github.com/14790897/paper-ai/commit/5e7afea400af03716d08e2d6d7aadb6eccd3448e))
* 去除暗色模式 ([da5cb4f](https://www.github.com/14790897/paper-ai/commit/da5cb4fac84e800e4e8bcc105a395088af615631))
* 只有在AI返回的内容没有错之后才能添加文献 ([3040c11](https://www.github.com/14790897/paper-ai/commit/3040c11ea1b843f923475ac117516e82de3f4458))
* 图标正常显示 ([8ab1bdd](https://www.github.com/14790897/paper-ai/commit/8ab1bdd3935713595d35ed41471146cfa83cc4f0))
* 导出word时文献不更新因为usecallback没有添加变量 ([01d3ffe](https://www.github.com/14790897/paper-ai/commit/01d3ffebf7a86c27161d97b73a949a1f0d73a62b))
* 论文引用格式 ([b603365](https://www.github.com/14790897/paper-ai/commit/b603365265314f61122a8bab0da1561bc1c0c5a2))
## [1.5.0](https://www.github.com/14790897/paper-ai/compare/v1.4.0...v1.5.0) (2024-02-13)
### Features
* pubmed论文格式更新 ([3636fe1](https://www.github.com/14790897/paper-ai/commit/3636fe1c358e3f4edebca2c17f527383f787e7a4))
* 加了一个按钮用来控制鼠标点击段落中的上标跳转到文献引用 ([e5d267c](https://www.github.com/14790897/paper-ai/commit/e5d267cf7075d8830bee172d2f8e5b6b1f487487))
* 更准确的文献引用 ([c4d4100](https://www.github.com/14790897/paper-ai/commit/c4d410073caea3cdc7e016e492b62c22fec99543))
* 记录selection位置以及自动清空UserInput内容 ([9e85552](https://www.github.com/14790897/paper-ai/commit/9e85552d8966f7408e2ae61b92b91d0b3ae40e59))
### Bug Fixes
* 删除段落的错误 ([089a7af](https://www.github.com/14790897/paper-ai/commit/089a7afa7e84ccd965e9a1d51a2cae47c0269b3e))
* 未选中焦点的时候会报错 ([342d78b](https://www.github.com/14790897/paper-ai/commit/342d78bd882367cbf9afbfb234f85bfb05e3d489))
## [1.4.0](https://www.github.com/14790897/paper-ai/compare/v1.3.0...v1.4.0) (2024-02-12)
### Features
* i18n 切换 ([2573950](https://www.github.com/14790897/paper-ai/commit/2573950ad82140a16d1d8d6c48d33fcfc269d81e))
* i18n中文英文 ([d64295e](https://www.github.com/14790897/paper-ai/commit/d64295e27ad3668539be8cb3b8b46bbccf086334))
* sentry ([f5ee326](https://www.github.com/14790897/paper-ai/commit/f5ee32669c23ceb79ea618dd04a8e781eabb4936))
* sentry追踪用户 ([b983427](https://www.github.com/14790897/paper-ai/commit/b983427721dda54aac187c9b95f8d21988944c06))
### Bug Fixes
* reference中的journal终于搞定 ([67212d6](https://www.github.com/14790897/paper-ai/commit/67212d6a8514c67d8d9e19733ba1d228c0690eed))
## [1.3.0](https://www.github.com/14790897/paper-ai/compare/v1.2.0...v1.3.0) (2024-02-10)
### Features
* lemonsqueezy1 ([fbd899c](https://www.github.com/14790897/paper-ai/commit/fbd899cae3d2a1daf41e3578a5b04d258da42b99))
* supa utils ([e7567cb](https://www.github.com/14790897/paper-ai/commit/e7567cb49d132059b03d7296e402d40a287cff31))
* supa路由 ([20add6b](https://www.github.com/14790897/paper-ai/commit/20add6b617cc9d205299f4ca02758b0ac55639ad))
* vip按钮 ([aeff960](https://www.github.com/14790897/paper-ai/commit/aeff96032ef4fa5d20ee62ee6a0813778095c726))
* 使用免费的deepseek ([10e7ef0](https://www.github.com/14790897/paper-ai/commit/10e7ef05c97bb3faff1563995b460defed72f1cb))
* 删除引用的时候同时删除段落 ([65da583](https://www.github.com/14790897/paper-ai/commit/65da583258346d8259707eb828d61b4e7790ec48))
* 在设置界面保存了多个可用的配置 ([a723425](https://www.github.com/14790897/paper-ai/commit/a72342504fc4b10498258c84a5fad812a32dee04))
* 完善了错误处理 ([91ab703](https://www.github.com/14790897/paper-ai/commit/91ab703708979e7c4f4bbc64793274db9e6c01bf))
* 导出word功能增加导出文献内容 ([8dcb54a](https://www.github.com/14790897/paper-ai/commit/8dcb54a49b46aed8441e28aeaf3e4489d9bae61a))
* 尝试删除索引的时候删除整个段落 ([732dd73](https://www.github.com/14790897/paper-ai/commit/732dd738c93601a0cd81379ced2dfa7ddd8ce683))
* 引用和内容分开更新 ([0276ff8](https://www.github.com/14790897/paper-ai/commit/0276ff8964486c89519e37265adbab5072e6c1aa))
* 注册时在profiles插入用户信息方便设置vip时读取email操作 ([8629d20](https://www.github.com/14790897/paper-ai/commit/8629d2034112165c16deb8f3f50f5f43899d1cd2))
* 点击引用数字调转到对应文献 ([3580c34](https://www.github.com/14790897/paper-ai/commit/3580c34e830d121a702a7cdbfaa5ed3a3c7a44bc))
* 管理云端多篇论文功能 ([fe31198](https://www.github.com/14790897/paper-ai/commit/fe31198124f9459c579260018ba673a1353b077f))
* 编辑过程中同步云端 ([2ca6d3d](https://www.github.com/14790897/paper-ai/commit/2ca6d3d212861aa6b54ac6beddd1e498026631ce))
* 自动识别最近的文献序号在那之后插入新的文献 ([ba8722a](https://www.github.com/14790897/paper-ai/commit/ba8722afdaf5698732521c1ad5be1ab8039a1655))
### Bug Fixes
* AI输入的时候鼠标可以失去焦点 ([88063ba](https://www.github.com/14790897/paper-ai/commit/88063baa2ed07ebb807b21138054a9805d948da0))
* isVip改为redux状态 ([91e9759](https://www.github.com/14790897/paper-ai/commit/91e9759cb7192901e88dd72699b33caad066a8ac))
* lemon ([6e6cf16](https://www.github.com/14790897/paper-ai/commit/6e6cf16fbadafc9a990df8eac8d9f14d8a67fca7))
* lemon ([acae014](https://www.github.com/14790897/paper-ai/commit/acae014a46ee8f3c32e12f5da820145ba8090eae))
* redux ([4fa7796](https://www.github.com/14790897/paper-ai/commit/4fa779698ec308dda603f54cb29cd718b5df41af))
* settings的redux ([9ee43eb](https://www.github.com/14790897/paper-ai/commit/9ee43ebd061498d1a03c14e6adef78840195bfd8))
* settings的redux ([1d4fbaf](https://www.github.com/14790897/paper-ai/commit/1d4fbaf8e426762b1b80f0d8d4761e835b8ac5da))
* system prompt可以输入 ([02357cc](https://www.github.com/14790897/paper-ai/commit/02357cc03661a0cda62413deaf07153cc117ceed))
* vercel部署密钥问题 ([84e0363](https://www.github.com/14790897/paper-ai/commit/84e0363313487020b97d0056c65f4a26f10f4cae))
* 修复pubmed articleUrl 无法正常获取的问题 ([f5ae3c1](https://www.github.com/14790897/paper-ai/commit/f5ae3c1ff456bdb6131a8c39b1d04d0ee2094db7))
* 修复service-role无法访问 ([9cb214d](https://www.github.com/14790897/paper-ai/commit/9cb214d67f7453a6c08d643957996a5ffa3b1110))
* 修复service-role无法访问 ([d0e6a72](https://www.github.com/14790897/paper-ai/commit/d0e6a72f0d57b3ad27e47676556acff8be13debf))
* 刷新之后不获取云端文献引用 ([74486f9](https://www.github.com/14790897/paper-ai/commit/74486f95c2f95b5e1cc6e031e0dddff52fbca15e))
* 将输入栏变得方便 ([165f189](https://www.github.com/14790897/paper-ai/commit/165f189efa7bd0003dd2a35b6acbcd040961198f))
* 引用改变的时候立刻同步 ([01d703a](https://www.github.com/14790897/paper-ai/commit/01d703a17b2b18c7bfead1080426f8f8d3619a36))
* 插入前先换行 ([d56f427](https://www.github.com/14790897/paper-ai/commit/d56f427484d342e893c9ff104f06ddb64e14f145))
* 撤销 ([f761c35](https://www.github.com/14790897/paper-ai/commit/f761c357ea3bf74a11a890e9da942db7c4e7fd4a))
* 文献引用删除修复 ([4e6f628](https://www.github.com/14790897/paper-ai/commit/4e6f628289063f28768bd05f92d23895c7417a27))
* 自动保存编辑器内容 ([6107267](https://www.github.com/14790897/paper-ai/commit/610726712366ca66c7392560f32000cf7e63a87f))
* 解决vip状态不正确因为没有使用dispatch ([833e2e1](https://www.github.com/14790897/paper-ai/commit/833e2e1b0ec0aac46d7759ac44172432fa31a6f0))
* 解决向云端同步的时候文章内容不更新 ([2286a48](https://www.github.com/14790897/paper-ai/commit/2286a48fc040c972d65f8c2a15c4701d31658869))
* 解决因为闭包导致paperNumberRedux 不正确的问题 ([486c75d](https://www.github.com/14790897/paper-ai/commit/486c75d4d7a9a016399170274bd55bab00f2c3b6))
## [1.2.0](https://www.github.com/14790897/paper-ai/compare/v1.1.0...v1.2.0) (2024-01-29)
### Features
* 上下移动文献功能 ([c7cef37](https://www.github.com/14790897/paper-ai/commit/c7cef370d0568c7bc1a4df798e624bc4494344d2))
* 导出到word功能避免样式丢失 ([cc2856c](https://www.github.com/14790897/paper-ai/commit/cc2856ceb21532fa1bd8d36b78b028fd627aa726))
* 系统提示自定义 ([c160d3e](https://www.github.com/14790897/paper-ai/commit/c160d3e6af970911e0f0163e0ab2979bdf79b8ad))
## [1.1.0](https://www.github.com/14790897/paper-ai/compare/v1.0.0...v1.1.0) (2024-01-28)

34
Dockerfile Normal file
View File

@ -0,0 +1,34 @@
# 使用 Node.js 官方镜像作为构建环境
FROM node:alpine as builder
# 设置工作目录
WORKDIR /app
# 复制 package.json 和 package-lock.json (或 yarn.lock)
COPY package*.json ./
# 安装项目依赖
RUN npm install
# 复制项目文件到工作目录
COPY . .
# 构建应用
RUN npm run build
# 使用 Node.js 镜像运行应用
FROM node:alpine
# 设置工作目录
WORKDIR /app
# 只复制构建产出和package.json到新的镜像中
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
# 暴露端口
EXPOSE 3000
# 启动 Next.js 应用
CMD ["npm", "start"]

View File

@ -1,30 +1,35 @@
[English Documentation](./README_en.md)
<a href="https://paperai.life">
<img alt="Next.js and Supabase Starter Kit - the fastest way to build apps with Next.js and Supabase" src="https://paperai.life/opengraph-image.png">
<h1 align="center">paper-ai</h1>
<div align="center">
<img src="./public/android-chrome-192x192.png" alt="the fastest way to create a paper with real references">
</div>
<h1 align="center">paper-ai</h1>
</a>
<p align="center"> <a href="./README_en.md"><b>English Documentation </b></a> </p>
<p align="center">
使用真实文献最快速完成论文的方法
</p>
<p align="center">
<a href='https://docs.paperai.life/' style='font-size: 20px;'><strong>文档网站(教程比较详细,推荐阅读这里)</strong></a> ·
<a href='https://www.bilibili.com/video/BV1Ya4y1k75V'><strong>bilibili视频教程</strong></a>
</p>
<p align="center">
<a href="#功能"><strong>功能</strong></a> ·
<a href="#演示"><strong>演示</strong></a> ·
<a href="#部署到 Vercel"><strong>部署到 Vercel</strong></a> ·
<a href="#克隆并在本地运行"><strong>克隆并在本地运行y</strong></a> ·
<a href="#部署到Vercel"><strong>部署到 Vercel</strong></a> ·
<a href="#克隆并在本地运行"><strong>克隆并在本地运行</strong></a>
</p>
<br/>
## 功能
### 利用人工智能撰写论文
- **人工智能书写功能** 点击 "AI 写作 "进行正常对话互动。人工智能将根据您的输入提供写作建议或回答问题。
- **Paper2AI功能** 点击 "Paper2AI"根据输入的关键词在Semantic Scholar或arxiv中搜索论文。系统将把信息整合到您的论文中。
- **寻找文献功能** 点击 "寻找文献",根据输入的关键词在 Semantic Scholar 或 arxiv 或 PubMed 中搜索论文。系统将把信息整合到您的论文中。
### 编辑和修改
@ -41,7 +46,31 @@
上述操作还会将 repo 克隆到 GitHub。
如果只想在本地开发,而不想部署到 Vercel[请按以下步骤操作](#clone-and-run-locally)。
如果只想在本地开发,而不想部署到 Vercel[请按以下步骤操作](#克隆并在本地运行)。
## 镜像运行
1. 拉取镜像
```sh
docker pull 14790897/paperai:latest
```
2. 运行镜像
```sh
docker run -d -p 3000:3000 \
-e NEXT_PUBLIC_AI_URL=自定义AI模型地址\
-e NEXT_PUBLIC_OPENAI_API_KEY=自定义API KEY \
14790897/paperai:latest
```
## 环境变量说明
1. NEXT_PUBLIC_OPENAI_API_KEY 设置 key只要在设置界面右上角齿轮对应的位置留空就会使用预定的变量
2. NEXT_PUBLIC_AI_URL 设置上游 url只要在设置界面右上角齿轮对应的位置留空就会使用预定的变量
3. NEXT_PUBLIC_SEMANTIC_API_KEY 设置 semantic scholar 的 key可以增加请求量
4. NEXT_PUBLIC_PUBMED_API_KEY 设置 pubmed 的 key可以增加请求量
## 克隆并在本地运行
@ -58,7 +87,14 @@ npm install
# 运行项目
npm run dev
```
```
## 参考文档
1. semantic scholar api: https://api.semanticscholar.org/api-docs/#tag/Paper-Data/operation/get_graph_paper_relevance_search
2. pubmed api: https://www.ncbi.nlm.nih.gov/books/NBK25500/
3. i18n: https://locize.com/blog/next-app-dir-i18n/
## 许可证
MIT
该项目已获得[MIT License](LICENSE)的许可

View File

@ -1,17 +1,24 @@
<a href="https://paperai.life">
<img alt="Next.js and Supabase Starter Kit - the fastest way to build apps with Next.js and Supabase" src="https://paperai.life/opengraph-image.png">
<h1 align="center">paper-ai</h1>
<div align="center">
<img src="./public/android-chrome-192x192.png" alt="the fastest way to create a paper with real references">
</div>
<h1 align="center">paper-ai</h1>
</a>
<p align="center">
The fastest way to write a paper with true references
</p>
<p align="center">
<a href='https://docs.paperai.life/' style='font-size: 20px;'><strong> Website Documentation (detailed tutorials, highly recommended)</strong></a> ·
<a href='https://www.bilibili.com/video/BV1Ya4y1k75V'><strong>bilibili Video Tutorial</strong></a>
</p>
<p align="center">
<a href="#features"><strong>Features</strong></a> ·
<a href="#demo"><strong>Demo</strong></a> ·
<a href="#deploy-to-vercel"><strong>Deploy to Vercel</strong></a> ·
<a href="#clone-and-run-locally"><strong>Clone and run locally</strong></a> ·
<a href="#clone-and-run-locally"><strong>Clone and run locally</strong></a>
<!-- <a href="#feedback-and-issues"><strong>Feedback and issues</strong></a>
<a href="#more-supabase-examples"><strong>More Examples</strong></a> -->
</p>
@ -40,6 +47,31 @@ The above will also clone the repo to your GitHub, you can clone that locally an
If you wish to just develop locally and not deploy to Vercel, [follow the steps below](#clone-and-run-locally).
## Using Docker
1. Using `docker pull` command
```sh
docker pull 14790897/paperai:latest
```
2. Run Docker
```sh
docker run -d -p 3000:3000 \
-e NEXT_PUBLIC_AI_URL=CUSTOM_AI_URL \
-e NEXT_PUBLIC_OPENAI_API_KEY=CUSTOM_API_KEY \
14790897/paperai:latest
```
Replace `CUSTOM_AI_URL` and `CUSTOM_API_KEY` to your own AI URL and API key
## Environment variable description
1. NEXT_PUBLIC_OPENAI_API_KEY sets the key. Simply leave the corresponding position in the settings interface (the gear in the upper right corner) blank, the predetermined variable will be used.
2. NEXT_PUBLIC_AI_URL sets the upstream url. Simply leave the corresponding position in the settings interface (the gear in the upper right corner) blank, the predetermined variable will be used.
3. NEXT_PUBLIC_SEMANTIC_API_KEY sets the `semantic scholar` key to increase the number of requests
4. NEXT_PUBLIC_PUBMED_API_KEY sets the `pubmed` key to increase the number of requests
## Clone and run locally
```bash
@ -57,5 +89,13 @@ npm run dev
```
## Reference
1. semantic scholar api: https://api.semanticscholar.org/api-docs/#tag/Paper-Data/operation/get_graph_paper_relevance_search
2. pubmed api: https://www.ncbi.nlm.nih.gov/books/NBK25500/
3. i18n: https://locize.com/blog/next-app-dir-i18n/
## LICENSE
MIT
This repository is licensed under the MIT License
See the [LICENSE](LICENSE) file for details.

View File

@ -1,30 +0,0 @@
"use client"; // Error components must be Client Components
import { useEffect } from "react";
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
// Log the error to an error reporting service
console.error(error);
}, [error]);
return (
<div>
<h2>Something went wrong!</h2>
<button
onClick={
// Attempt to recover by trying to re-render the segment
() => reset()
}
>
Try again
</button>
</div>
);
}

View File

@ -1,12 +0,0 @@
"use client";
import ReduxProvider from "@/app/store/ReduxProvider";
import QEditor from "../components/QuillEditor";
export default function QuillWrapper() {
return (
<ReduxProvider>
<QEditor />
</ReduxProvider>
);
}

View File

@ -0,0 +1,51 @@
import { createClient } from "@/utils/supabase/server";
import { cookies } from "next/headers";
export async function POST(req: Request) {
const cookieStore = cookies();
const supabaseAdmin = createClient(cookieStore);
// 从请求体中提取数据
const { userId, paperContent, paperReference, paperNumber } =
await req.json();
// 使用Supabase客户端进行数据上传
const { data, error } = await supabaseAdmin.from("user_paper").upsert(
[
{
user_id: userId,
paper_number: paperNumber,
...(paperContent !== undefined && { paper_content: paperContent }),
...(paperReference !== undefined && {
paper_reference: paperReference,
}),
},
],
{ onConflict: "user_id, paper_number" }
);
// console.log("测试supabaseAdmin", supabaseAdmin);
// 返回JSON格式的响应
if (error) {
// 如果有错误,返回错误信息
return new Response(
JSON.stringify({ message: "Error saving paper", error: error.message }),
{
status: 400, // 或其他适当的错误状态码
headers: {
"Content-Type": "application/json",
},
}
);
} else {
// 成功保存,返回成功信息
return new Response(
JSON.stringify({ message: "Success in user_paper save", data }),
{
status: 200,
headers: {
"Content-Type": "application/json",
},
}
);
}
}

View File

@ -0,0 +1,63 @@
import { createClient } from "@/utils/supabase/server";
import { cookies } from "next/headers";
import { supabaseAdmin } from "@/utils/supabase/servicerole";
export async function POST(req: Request) {
try {
// const cookieStore = cookies();
// const supabaseAdmin = createClient(cookieStore);
// const {
// data: { user },
// } = await supabaseAdmin.auth.getUser();
// // 从请求体中提取数据
// if (!user) throw new Error("No user found");
// const userId = await user.id;
const { userId } = await req.json();
console.log("userId", userId);
const { data, error } = await supabaseAdmin
.from("user_paper") // 指定表名
.select("paper_number") // 仅选择paper_number列
.eq("user_id", userId); // 筛选特定user_id的记录
// 返回JSON格式的响应
if (error) {
// 如果有错误,返回错误信息
return new Response(
JSON.stringify({
message: "Error get paper numbers",
error: error.message,
}),
{
status: 400, // 或其他适当的错误状态码
headers: {
"Content-Type": "application/json",
},
}
);
} else {
console.log("获取到的用户论文数量:", data);
// 成功保存,返回成功信息
return new Response(JSON.stringify(data), {
status: 200,
headers: {
"Content-Type": "application/json",
},
});
}
} catch (e) {
console.error("Error get paper numbers", e);
return new Response(
JSON.stringify({
message: "Error get paper numbers",
error: e.message,
}),
{
status: 400, // 或其他适当的错误状态码
headers: {
"Content-Type": "application/json",
},
}
);
}
}

View File

@ -0,0 +1,43 @@
import { createClient } from "@/utils/supabase/server";
import { cookies } from "next/headers";
export async function POST(req: Request) {
const cookieStore = cookies();
const supabaseAdmin = createClient(cookieStore);
// 从请求体中提取数据
const { userId, paperNumber } = await req.json();
// 使用Supabase客户端进行数据上传
const { data, error } = await supabaseAdmin
.from("user_paper") // 指定表名
.select("paper_content,paper_reference") // 仅选择paper_content列
.eq("user_id", userId) // 筛选特定user_id的记录
.eq("paper_number", paperNumber)
.single(); // 筛选特定paper_number的记录
// 返回JSON格式的响应
if (error) {
// 如果有错误,返回错误信息
return new Response(
JSON.stringify({
message: "Error get specific paper",
error: error.message,
}),
{
status: 400, // 或其他适当的错误状态码
headers: {
"Content-Type": "application/json",
},
}
);
} else {
// 成功保存,返回成功信息
return new Response(JSON.stringify(data), {
status: 200,
headers: {
"Content-Type": "application/json",
},
});
}
}

View File

177
app/[lng]/login/page.tsx Normal file
View File

@ -0,0 +1,177 @@
import Link from "next/link";
import { headers, cookies } from "next/headers";
import { createClient } from "@/utils/supabase/server";
import { redirect } from "next/navigation";
import * as Sentry from "@sentry/nextjs";
import DeployButton from "@/components/DeployButton";
import SettingsLink from "@/components/SettingsLink";
//i18n
import { useTranslation } from "@/app/i18n";
import { FooterBase } from "@/components/Footer/FooterBase";
//supabase
import { insertUserProfile } from "@/utils/supabase/supabaseutils";
// SignInWithProvider
import { SignInWithProvider } from "@/components/SignInWithProvider";
import LinuxdoSignin from "@/components/LinuxdoSignin";
export default async function Login({
searchParams,
params: { lng },
}: {
searchParams: { message: string };
params: { lng: string };
}) {
const { t } = await useTranslation(lng);
const signIn = async (formData: FormData) => {
"use server";
const email = formData.get("email") as string;
const password = formData.get("password") as string;
const cookieStore = cookies();
const supabase = createClient(cookieStore);
const { data, error } = await supabase.auth.signInWithPassword({
email,
password,
});
//sentry
const user = data?.user;
if (user && process.env.NODE_ENV === "production") {
Sentry.setUser({
email: user.email,
id: user.id,
ip_address: "{{auto}}}",
});
}
if (error) {
return redirect("/login?message=Could not authenticate user");
}
return redirect("/");
};
const signUp = async (formData: FormData) => {
"use server";
const origin = headers().get("origin");
const email = formData.get("email") as string;
const password = formData.get("password") as string;
const cookieStore = cookies();
const supabase = createClient(cookieStore);
const { data, error } = await supabase.auth.signUp({
email,
password,
options: {
emailRedirectTo: `${origin}/auth/callback`,
},
});
//profiles表 插入用户信息
await insertUserProfile(data, supabase);
if (error) {
return redirect("/login?message=Could not authenticate user");
}
return redirect("/login?message=Check email to continue sign in process");
};
return (
<div className="flex-1 flex flex-col w-full px-8 sm:max-w-md justify-center gap-2">
<nav className="w-full flex justify-center border-b border-b-foreground/10 h-16">
<div className="w-full max-w-4xl flex justify-between items-center p-3 text-sm">
<DeployButton />
<SettingsLink />
</div>
</nav>
<Link
href="/"
className="absolute left-8 top-8 py-2 px-4 rounded-md no-underline text-foreground bg-btn-background hover:bg-btn-background-hover flex items-center group text-sm"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="mr-2 h-4 w-4 transition-transform group-hover:-translate-x-1"
>
<polyline points="15 18 9 12 15 6" />
</svg>{" "}
Back
</Link>
<form
className="animate-in flex-1 flex flex-col w-full justify-center gap-2 text-foreground"
action={signIn}
>
<label className="text-md" htmlFor="email">
Email
</label>
<input
className="rounded-md px-4 py-2 bg-inherit border mb-6"
name="email"
placeholder="you@example.com"
required
/>
<label className="text-md" htmlFor="password">
Password
</label>
<input
className="rounded-md px-4 py-2 bg-inherit border mb-6"
type="password"
name="password"
placeholder="••••••••"
required
/>
<button className="bg-green-700 rounded-md px-4 py-2 text-foreground mb-2">
Sign In
</button>
<button
formAction={signUp}
className="border border-foreground/20 rounded-md px-4 py-2 text-foreground mb-2"
>
Sign Up
</button>
{/* 重置密码 */}
<button className="border border-foreground/20 rounded-md px-4 py-2 text-foreground mb-2">
<Link href="/request-reset">Reset Password</Link>
</button>
{searchParams?.message && (
<p className="mt-4 p-4 bg-foreground/10 text-foreground text-center">
{searchParams.message}
</p>
)}
</form>
<div>
<LinuxdoSignin />
<SignInWithProvider
provider="github"
redirectTo="https://www.paperai.life/welcome"
/>
<SignInWithProvider
provider="google"
redirectTo="https://www.paperai.life/welcome"
/>
</div>
<footer className="w-full border-t border-t-foreground/10 p-8 flex justify-center text-center text-xs">
<div className="flex items-center space-x-4">
{" "}
{/* 添加flex容器来水平排列子元素 */}
<a
href="https://github.com/14790897/paper-ai"
target="_blank"
className="font-bold text-blue-600 hover:underline hover:text-blue-800"
rel="noreferrer"
>
{t("give me a star in GitHub")}
</a>
<FooterBase t={t} lng={lng} />
</div>
</footer>
</div>
);
}

112
app/[lng]/page.tsx Normal file
View File

@ -0,0 +1,112 @@
import PaperListButtonWrapper from "@/components/PaperListButtonWrapper";
import AuthButton from "@/components/AuthButton";
import { createClient } from "@/utils/supabase/server";
import ConnectSupabaseSteps from "@/components/ConnectSupabaseSteps";
import SignUpUserSteps from "@/components/SignUpUserSteps";
import Header from "@/components/Header";
import { cookies } from "next/headers";
import QuillWrapper from "@/components/QuillWrapper";
// import TinyEditor from "../components/TinyEditor";
// import SEditor from "../components/SlateEditor";
import SettingsLink from "@/components/SettingsLink";
import PaperManagementWrapper from "@/components/PaperManagementWrapper";
//i18n
import { useTranslation } from "@/app/i18n";
import { FooterBase } from "@/components/Footer/FooterBase";
import { IndexProps } from "@/utils/global";
import GoogleSignIn from "@/components/GoogleSignIn";
// import Error from "@/app/global-error";
export default async function Index({ params: { lng } }: IndexProps) {
const { t } = await useTranslation(lng);
const cookieStore = cookies();
let supabase: any, user;
const canInitSupabaseClient = () => {
// This function is just for the interactive tutorial.
// Feel free to remove it once you have Supabase connected.
try {
supabase = createClient(cookieStore);
return true;
} catch (e) {
return false;
}
};
const isSupabaseConnected = canInitSupabaseClient();
if (supabase) {
({
data: { user },
} = await supabase.auth.getUser());
}
console.log("user in page", user);
return (
<div className="flex-1 w-full flex flex-col gap-5 items-center">
<nav className="w-full flex justify-center border-b border-b-foreground/10 h-16">
<div className="w-full max-w-4xl flex justify-between items-center p-3 text-sm">
{/* <DeployButton /> */}
{/* 用来表示是否显示论文列表页 */}
<PaperListButtonWrapper />
{isSupabaseConnected && <AuthButton />}
{/* 如果用户没有登录会出现谷歌的sign in按钮登录之后不会出现 */}
{!user && <GoogleSignIn />}
<SettingsLink />
</div>
</nav>
<PaperManagementWrapper lng={lng} />
<QuillWrapper lng={lng} />
<footer className="w-full border-t border-t-foreground/10 p-8 flex justify-center text-center text-xs">
<div className="flex items-center space-x-4">
{" "}
{/* 添加flex容器来水平排列子元素 */}
<a
href="https://github.com/14790897/paper-ai"
target="_blank"
className="font-bold text-blue-600 hover:underline hover:text-blue-800"
rel="noreferrer"
>
{t("give me a star in GitHub")}
</a>
<a
href="https://docs.paperai.life/"
target="_blank"
className="font-bold text-blue-500 hover:underline hover:text-blue-700"
>
<strong>使</strong>
</a>
<a
href="./privacy"
target="_blank"
className="font-bold text-blue-500 hover:underline hover:text-blue-700"
>
<strong>PrivacyPolicy</strong>
</a>
<a
href="./service"
target="_blank"
className="font-bold text-blue-500 hover:underline hover:text-blue-700"
>
<strong>Service</strong>
</a>
<FooterBase t={t} lng={lng} />
</div>
</footer>
</div>
);
}
{
/* <div className="animate-in flex-1 flex flex-col gap-20 opacity-0 max-w-4xl px-3">
<Header />
<main className="flex-1 flex flex-col gap-6">
<h2 className="font-bold text-4xl mb-4">Next steps</h2>
{isSupabaseConnected ? <SignUpUserSteps /> : <ConnectSupabaseSteps />}
</main>
</div> */
}
{
/* <div className="animate-in flex-1 flex flex-col gap-20 opacity-0 max-w-4xl px-3"> */
}
{
/*</div> */
}

View File

@ -0,0 +1,70 @@
import React from "react";
export default function PrivacyPolicy() {
return (
<div className="max-w-4xl mx-auto p-4">
<h1></h1>
<p>
使paperai使
</p>
<h2>使</h2>
<p>使paperai时</p>
<ul>
<li></li>
<li></li>
</ul>
<p>使</p>
<ul>
<li></li>
<li></li>
<li>使</li>
<li>Cookie</li>
</ul>
<p>使</p>
<ul>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
</ul>
<h2></h2>
<p>
</p>
<ul>
<li>便</li>
<li></li>
<li></li>
</ul>
<h2></h2>
<p>
访
</p>
<h2></h2>
<p>
访使
</p>
<h2></h2>
<p>
//
</p>
<h2></h2>
<p>
paperai上发布新的隐私政策来通知您任何更改
</p>
<h2></h2>
<p>
liuweiqing147@gmail.com过与我们联系
</p>
<p>2024.3.7</p>
</div>
);
}

View File

@ -0,0 +1,41 @@
"use client";
import { useState } from "react";
import { createClient } from "@/utils/supabase/client";
const RequestResetPassword = () => {
const supabase = createClient();
const [email, setEmail] = useState("");
const handleResetPassword = async () => {
const { data, error } = await supabase.auth.resetPasswordForEmail(email, {
redirectTo: `${window.location.origin}/reset-password`, // 确保这个URL是你重置密码页面的地址
});
console.log("当前链接", `${window.location.origin}/reset-password`);
if (error) {
alert("Error sending password reset email: " + error.message);
} else {
alert("Please check your email for the password reset link");
}
};
return (
<div className="flex flex-col items-center justify-center p-4">
<input
type="email"
placeholder="Your email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="px-4 py-2 border rounded-md focus:ring-blue-500 focus:border-blue-500 shadow-sm"
/>
<button
onClick={handleResetPassword}
className="mt-4 px-4 py-2 bg-blue-500 text-white rounded-md shadow hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50"
>
Reset Password
</button>
</div>
);
};
export default RequestResetPassword;

View File

@ -0,0 +1,58 @@
"use client";
import { useState } from "react";
import { createClient } from "@/utils/supabase/client";
import { useRouter } from "next/navigation";
const ResetPassword = () => {
const supabase = createClient();
const [newPassword, setNewPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const router = useRouter();
const handleNewPassword = async () => {
// 检查两次输入的密码是否一致
if (newPassword !== confirmPassword) {
alert("The passwords do not match. Please try again.");
return;
}
const { error } = await supabase.auth.updateUser({
password: newPassword,
});
if (error) {
alert("Error resetting password: " + error.message);
} else {
alert("Your password has been reset successfully.");
router.push("/login"); // 导航到登录页面或其他页面
}
};
return (
<div className="flex flex-col items-center justify-center space-y-4 bg-gray-50 p-6 rounded-lg shadow-md">
<input
type="password"
placeholder="New password新密码"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<input
type="password"
placeholder="Confirm new password确认新密码"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
onClick={handleNewPassword}
className="px-4 py-2 w-full text-white bg-blue-500 hover:bg-blue-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 transition-colors"
>
Update Password
</button>
</div>
);
};
export default ResetPassword;

View File

@ -0,0 +1,79 @@
"use client";
import Head from "next/head";
import * as Sentry from "@sentry/nextjs";
export default function Page() {
return (
<div>
<Head>
<title>Sentry Onboarding</title>
<meta name="description" content="Test Sentry for your Next.js app!" />
</Head>
<main
style={{
minHeight: "100vh",
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
}}
>
<h1 style={{ fontSize: "4rem", margin: "14px 0" }}>
<svg
style={{
height: "1em",
}}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 200 44"
>
<path
fill="currentColor"
d="M124.32,28.28,109.56,9.22h-3.68V34.77h3.73V15.19l15.18,19.58h3.26V9.22h-3.73ZM87.15,23.54h13.23V20.22H87.14V12.53h14.93V9.21H83.34V34.77h18.92V31.45H87.14ZM71.59,20.3h0C66.44,19.06,65,18.08,65,15.7c0-2.14,1.89-3.59,4.71-3.59a12.06,12.06,0,0,1,7.07,2.55l2-2.83a14.1,14.1,0,0,0-9-3c-5.06,0-8.59,3-8.59,7.27,0,4.6,3,6.19,8.46,7.52C74.51,24.74,76,25.78,76,28.11s-2,3.77-5.09,3.77a12.34,12.34,0,0,1-8.3-3.26l-2.25,2.69a15.94,15.94,0,0,0,10.42,3.85c5.48,0,9-2.95,9-7.51C79.75,23.79,77.47,21.72,71.59,20.3ZM195.7,9.22l-7.69,12-7.64-12h-4.46L186,24.67V34.78h3.84V24.55L200,9.22Zm-64.63,3.46h8.37v22.1h3.84V12.68h8.37V9.22H131.08ZM169.41,24.8c3.86-1.07,6-3.77,6-7.63,0-4.91-3.59-8-9.38-8H154.67V34.76h3.8V25.58h6.45l6.48,9.2h4.44l-7-9.82Zm-10.95-2.5V12.6h7.17c3.74,0,5.88,1.77,5.88,4.84s-2.29,4.86-5.84,4.86Z M29,2.26a4.67,4.67,0,0,0-8,0L14.42,13.53A32.21,32.21,0,0,1,32.17,40.19H27.55A27.68,27.68,0,0,0,12.09,17.47L6,28a15.92,15.92,0,0,1,9.23,12.17H4.62A.76.76,0,0,1,4,39.06l2.94-5a10.74,10.74,0,0,0-3.36-1.9l-2.91,5a4.54,4.54,0,0,0,1.69,6.24A4.66,4.66,0,0,0,4.62,44H19.15a19.4,19.4,0,0,0-8-17.31l2.31-4A23.87,23.87,0,0,1,23.76,44H36.07a35.88,35.88,0,0,0-16.41-31.8l4.67-8a.77.77,0,0,1,1.05-.27c.53.29,20.29,34.77,20.66,35.17a.76.76,0,0,1-.68,1.13H40.6q.09,1.91,0,3.81h4.78A4.59,4.59,0,0,0,50,39.43a4.49,4.49,0,0,0-.62-2.28Z"
></path>
</svg>
</h1>
<p>Get started by sending us a sample error:</p>
<button
type="button"
style={{
padding: "12px",
cursor: "pointer",
backgroundColor: "#AD6CAA",
borderRadius: "4px",
border: "none",
color: "white",
fontSize: "14px",
margin: "18px",
}}
onClick={() => {
Sentry.startSpan({
name: 'Example Frontend Span',
op: 'test'
}, async () => {
const res = await fetch("/api/sentry-example-api");
if (!res.ok) {
throw new Error("Sentry Example Frontend Error");
}
});
}}
>
Throw error!
</button>
<p>
Next, look for the error on the{" "}
<a href="https://liuweiqing-limited.sentry.io/issues/?project=4506728672264192">Issues Page</a>.
</p>
<p style={{ marginTop: "24px" }}>
For more information, see{" "}
<a href="https://docs.sentry.io/platforms/javascript/guides/nextjs/">
https://docs.sentry.io/platforms/javascript/guides/nextjs/
</a>
</p>
</main>
</div>
);
}

View File

@ -0,0 +1,50 @@
import React from "react";
const TermsOfService = () => {
return (
<div>
<h1></h1>
<p>2024.3.7</p>
<h2>1. </h2>
<p>
使paperai使使
</p>
<h2>2. 使</h2>
<p>
使/
</p>
<h2>3. </h2>
<p>
使
</p>
<h2>4. </h2>
<p>
使使
</p>
<h2>5. </h2>
<p>
</p>
<h2>6. </h2>
<p>
使
</p>
<h2>7. </h2>
<p>USA的法律进行解释和执行</p>
<h2>8. </h2>
<p>
liuweiqing147@gmail.com与我们联系
</p>
</div>
);
};
export default TermsOfService;

View File

@ -0,0 +1,12 @@
//这里是settings页面
import SettingsWrapper from "@/components/SettingsWrapper";
//i18n
import { IndexProps } from "@/utils/global";
export default function settings({ params: { lng } }: IndexProps) {
return (
<div className="h-screen w-full ">
<SettingsWrapper lng={lng} />
</div>
);
}

View File

@ -0,0 +1,46 @@
"use server";
import { createClient } from "@/utils/supabase/server";
import Link from "next/link";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import LoadingIndicator from "@/components/LoadingIndicator"; // 确保路径正确
import { insertUserProfile } from "@/utils/supabase/supabaseutils";
import React from "react";
export default async function WelcomeScreen() {
// const [isLoading, setIsLoading] = React.useState(true);
const cookieStore = cookies();
const supabase = createClient(cookieStore);
const {
data,
data: { user },
} = await supabase.auth.getUser();
//profiles表 插入用户信息
await insertUserProfile(data, supabase);
// setIsLoading(false);
//1秒后跳转到首页
// setTimeout(() => {
redirect("/");
// }, 1000);
return (
<>
{user ? (
<div className="flex items-center gap-4">
Hey, {user.email}!
<div style={{ margin: "20px", textAlign: "center" }}>
<h1>welcome, {user.email}!</h1>
</div>
</div>
) : (
<Link
href="/login"
className="py-2 px-3 flex rounded-md no-underline bg-btn-background hover:bg-btn-background-hover"
>
Login
</Link>
)}
<LoadingIndicator />
</>
);
}

View File

@ -0,0 +1,141 @@
// app/api/lemon/callback/route.ts
import { headers } from "next/headers";
import { Buffer } from "buffer";
import crypto from "crypto";
import rawBody from "raw-body";
import { Readable } from "stream";
import { NextResponse } from "next/server";
import { createClient } from "@/utils/supabase/server";
import { cookies } from "next/headers";
import { SupabaseClient } from "@supabase/supabase-js";
export async function POST(request: Request) {
const cookieStore = cookies();
const supabaseAdmin = createClient(cookieStore);
console.log("webhook");
const body = await rawBody(Readable.from(Buffer.from(await request.text())));
const headersList = headers();
const payload = JSON.parse(body.toString());
const sigString = headersList.get("x-signature");
if (!sigString) {
console.error(`Signature header not found`);
return NextResponse.json(
{ message: "Signature header not found" },
{ status: 401 }
);
}
try {
const secret = process.env.LEMONS_SQUEEZY_SIGNATURE_SECRET as string;
console.log("secret:", secret);
const hmac = crypto.createHmac("sha256", secret);
const digest = Buffer.from(hmac.update(body).digest("hex"), "utf8");
const signature = Buffer.from(
Array.isArray(sigString) ? sigString.join("") : sigString || "",
"utf8"
);
// 校验签名
if (!crypto.timingSafeEqual(digest, signature)) {
return NextResponse.json(
{ message: "Invalid signature" },
{ status: 403 }
);
}
console.log("payload:", payload);
const userEmail =
(payload.data.attributes && payload.data.attributes.user_email) || "";
// 检查custom里的参数
if (!userEmail)
return NextResponse.json(
{ message: "No userEmail provided" },
{ status: 403 }
);
return await setVip(supabaseAdmin, userEmail);
} catch (error) {
console.error("Error in lemon squeezy:", error);
return NextResponse.json(
{ message: "Error in lemon squeezy:", error },
{ status: 403 }
);
}
}
async function getUserId(supabaseAdmin: SupabaseClient, email: string) {
const { data, error } = await supabaseAdmin
.from("profiles")
.select("id")
.eq("email", email)
.single();
if (error) {
console.error("查询用户 ID 失败:", error);
return null;
}
return data.id;
}
async function setVip(
supabaseAdmin: SupabaseClient,
email: string,
isVip = true,
startDate = new Date(),
endDate = new Date()
) {
const userId = await getUserId(supabaseAdmin, email);
if (!userId)
return NextResponse.json({ message: "No user found" }, { status: 403 });
const { data, error } = await supabaseAdmin.from("vip_statuses").upsert(
{
user_id: userId,
is_vip: isVip,
start_date: startDate,
end_date: endDate,
},
{ onConflict: "user_id" }
);
if (error) {
console.error("设置 VIP 失败:", error);
return NextResponse.json(
{ message: "Failed to set VIP 设置 VIP 状态失败" },
{ status: 403 }
);
}
return NextResponse.json({ message: "Success VIP 状态已更新:" });
}
export async function GET(request: Request) {
// 创建一个简易的HTML内容
const htmlContent = `
<!DOCTYPE html>
<html>
<head>
<title>VIP Status Checker</title>
<style>
body, html {
height: 100%;
margin: 0;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
font-family: Arial, sans-serif;
}
div.container {
flex-direction: column;
}
</style>
</head>
<body>
<p>VIP的代码</p>
<a href="https://github.com/14790897/paper-ai/blob/main/app/api/lemon/callback/route.ts">route.ts</a>
</body>
</html>
`;
return new Response(htmlContent, {
headers: {
"Content-Type": "text/html; charset=UTF-8",
},
});
}

View File

@ -0,0 +1,106 @@
// api/oauth/callback.js
import { createClient } from "@/utils/supabase/server";
import { NextResponse } from "next/server";
import { cookies } from "next/headers";
import axios from "axios";
//supabase
import { insertUserProfile } from "@/utils/supabase/supabaseutils";
import { setVip } from "@/utils/supabase/serverutils";
export async function GET(request: Request) {
const requestUrl = new URL(request.url);
const code = requestUrl.searchParams.get("code");
if (code) {
const cookieStore = cookies();
const supabase = createClient(cookieStore);
// await supabase.auth.exchangeCodeForSession(code);
// 使用授权码请求访问令牌
const tokenResponse = await getToken(code);
const accessToken = tokenResponse!.data.access_token;
console.log("accessToekn", accessToken);
// 使用访问令牌获取用户信息
const userResponse = await axios.get("https://connect.linux.do/api/user", {
headers: { Authorization: `Bearer ${accessToken}` },
});
const userInfo = userResponse.data;
const uuid = "9e1c30b5-723c-4805-b3b8-0ac3c1923514"; //生成密码
let userId = null;
// 尝试注册新用户
const signUpResponse = await supabase.auth.signUp({
email: `${userInfo.username}@linux.do`, // 使用模板字符串构建email
password: uuid, // 使用uuid作为密码
});
if (signUpResponse.error) {
// 如果用户已存在,尝试登录来获取用户信息
await supabase.auth.signOut();
const signInResponse = await supabase.auth.signInWithPassword({
email: `${userInfo.username}@linux.do`,
password: uuid,
});
if (signInResponse.error) {
console.error("Error logging in existing user:", signInResponse.error);
// 处理登录失败的情况
} else {
//signin成功
userId = signInResponse.data.user!.id;
}
} else {
//signup成功之后可能要signin一次
const signInResponse = await supabase.auth.signInWithPassword({
email: `${userInfo.username}@linux.do`,
password: uuid,
});
console.log("signInResponse:", signInResponse);
userId = signUpResponse.data.user!.id;
}
// 如果获取到了用户ID进行后续操作
if (userId) {
// console.log("signUpResponse.data:", signUpResponse.data);
//插入信息并设置VIP
await insertUserProfile(signUpResponse.data, supabase);
await setVip(supabase, userId, true, "Linuxdo");
} else {
return new Response(
JSON.stringify({ error: "Unable to register or login the user" }),
{
status: 500,
headers: {
"Content-Type": "application/json",
},
}
);
}
// URL to redirect to after sign in process completes
return NextResponse.redirect(requestUrl.origin);
}
async function getToken(code: string) {
// 使用client_id和client_secret创建Basic Auth凭证
try {
const tokenResponse = await axios.post(
"https://connect.linux.do/oauth2/token",
`grant_type=authorization_code&code=${code}&redirect_uri=${encodeURIComponent(
process.env.REDIRECT_URI
)}`,
{
headers: {
Authorization: `Basic ${Buffer.from(
`${process.env.NEXT_PUBLIC_CLIENT_ID}:${process.env.CLIENT_SECRET}`
).toString("base64")}`,
"Content-Type": "application/x-www-form-urlencoded",
},
}
);
// 处理tokenResponse...
return tokenResponse;
} catch (error) {
// 处理错误...
console.error(error);
}
}
}

View File

@ -0,0 +1,9 @@
import { NextResponse } from "next/server";
export const dynamic = "force-dynamic";
// A faulty API route to test Sentry's error monitoring
export function GET() {
throw new Error("Sentry Example API Route Error");
return NextResponse.json({ data: "Testing Sentry Error..." });
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 15 KiB

19
app/global-error.jsx Normal file
View File

@ -0,0 +1,19 @@
"use client";
import * as Sentry from "@sentry/nextjs";
import Error from "next/error";
import { useEffect } from "react";
export default function GlobalError({ error }) {
useEffect(() => {
Sentry.captureException(error);
}, [error]);
return (
<html>
<body>
<Error />
</body>
</html>
);
}

View File

@ -10,14 +10,14 @@
--foreground: 200 50% 3%;
}
@media (prefers-color-scheme: dark) {
/* @media (prefers-color-scheme: dark) {
:root {
--background: 200 50% 3%;
--btn-background: 200 10% 9%;
--btn-background-hover: 200 10% 12%;
--foreground: 200 20% 96%;
}
}
} */
}
@layer base {
@ -40,3 +40,135 @@
transform: translateY(0);
}
}
.animate-slide-in-right {
animation: slideInFromRight 0.5s ease-out forwards;
}
@keyframes slideInFromRight {
0% {
transform: translateX(100%); /* 从右侧外开始 */
}
100% {
transform: translateX(0); /* 完全进入视图 */
}
}
.component-container {
@apply fixed top-1/4 right-0;
transform: translateX(100%); /* 动画开始前,确保组件位于视图右侧之外 */
}
/* 想给上标添加一个鼠标放上去变手型的效果 */
.ql-editor .ql-super {
cursor: pointer;
}
@keyframes flash {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.vip-icon {
animation: flash 1s linear infinite;
}
/* 动画的基本样式 */
.slide-enter {
opacity: 0;
transform: translateY(100%); /* 从底部滑入 */
}
.slide-enter-active {
opacity: 1;
transform: translateY(0);
transition: opacity 300ms ease-out, transform 300ms ease-out;
}
.slide-exit {
opacity: 1;
}
.slide-exit-active {
opacity: 0;
transform: translateY(100%); /* 向底部滑出 */
transition: opacity 300ms ease-in, transform 300ms ease-in;
}
.paper-management-container {
position: fixed; /* 或者使用 `absolute` 根据需要 */
top: 50%; /* 调整到视口的垂直中心 */
left: 50%; /* 调整到视口的水平中心 */
background-color: rgba(255, 255, 255, 0.5);
transform: translate(
-50%,
-50%
); /* 从中心点向上和向左偏移自身的50%,确保组件居中 */
z-index: 1000; /* 确保悬浮层在其他内容之上 */
/* 可以添加其他样式来美化组件,如背景色、阴影等 */
}
#editor {
/* width: calc(100vw - 20px); */
min-height: 250px;
max-height: 400px;
overflow-y: auto;
border: 1px solid #ccc;
}
/* 适配手机样式 */
@media (max-width: 768px) {
#editor {
width: 100%; /* 适应屏幕宽度 */
min-height: 200px; /* 调整为更适合移动设备的尺寸 */
}
#Qtoolbar {
display: flex; /* 使用弹性盒布局 */
flex-direction: row; /* 项目水平排列 */
flex-wrap: wrap; /* 允许项目换行 */
align-items: center; /* 项目在交叉轴上居中对齐 */
justify-content: center; /* 项目在主轴上靠左对齐 */
}
#Qtoolbar > textarea {
width: 100%;
min-height: 60px; /* 调整输入框的高度 */
}
#Qtoolbar > button,
#Qtoolbar > select,
#Qtoolbar > input {
margin-bottom: 10px; /* 增加元素之间的间距 */
margin-right: 10px; /* 右边距,增加元素之间的间距 */
}
}
/* 输入框基本样式 */
.textarea-focus-expand {
height: 50px; /* 默认高度 */
flex-grow: 1;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
border: 1px solid #ccc;
border-radius: 4px;
padding: 10px 15px;
margin-right: 8px;
color: #333;
transition: height 0.3s ease; /* 平滑过渡效果 */
}
/* 输入框获得焦点时的样式 */
.textarea-focus-expand:focus {
height: 100px; /* 聚焦时的高度 */
border-color: #007bff; /* 改变边框颜色以提供视觉反馈 */
}
.icon-hover:hover {
transform: scale(1.2); /* 放大到原大小的1.2倍 */
transition: transform 0.3s ease; /* 平滑过渡效果 */
}
/* 去除toast的最大宽度限制 */
.Toastify__toast-container--top-center:has(div.toastDetail) {
width: 80%;
}

60
app/i18n/client.js Normal file
View File

@ -0,0 +1,60 @@
"use client";
import { useEffect, useState } from "react";
import i18next from "i18next";
import {
initReactI18next,
useTranslation as useTranslationOrg,
} from "react-i18next";
import { useCookies } from "react-cookie";
import resourcesToBackend from "i18next-resources-to-backend";
import LanguageDetector from "i18next-browser-languagedetector";
import { getOptions, languages, cookieName } from "./settings";
const runsOnServerSide = typeof window === "undefined";
//
i18next
.use(initReactI18next)
.use(LanguageDetector)
.use(
resourcesToBackend((language, namespace) =>
import(`./locales/${language}/${namespace}.json`)
)
)
.init({
...getOptions(),
lng: undefined, // let detect the language on client side
detection: {
order: ["path", "htmlTag", "cookie", "navigator"],
},
preload: runsOnServerSide ? languages : [],
});
export function useTranslation(lng, ns, options) {
const [cookies, setCookie] = useCookies([cookieName]);
const ret = useTranslationOrg(ns, options);
const { i18n } = ret;
if (runsOnServerSide && lng && i18n.resolvedLanguage !== lng) {
i18n.changeLanguage(lng);
} else {
// eslint-disable-next-line react-hooks/rules-of-hooks
const [activeLng, setActiveLng] = useState(i18n.resolvedLanguage);
// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => {
if (activeLng === i18n.resolvedLanguage) return;
setActiveLng(i18n.resolvedLanguage);
}, [activeLng, i18n.resolvedLanguage]);
// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => {
if (!lng || i18n.resolvedLanguage === lng) return;
i18n.changeLanguage(lng);
}, [lng, i18n]);
// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => {
if (cookies.i18next === lng) return;
setCookie(cookieName, lng, { path: "/" });
}, [lng, cookies.i18next]);
}
return ret;
}

29
app/i18n/index.js Normal file
View File

@ -0,0 +1,29 @@
import { createInstance } from "i18next";
import resourcesToBackend from "i18next-resources-to-backend";
import { initReactI18next } from "react-i18next/initReactI18next";
import { getOptions } from "./settings";
const initI18next = async (lng, ns) => {
const i18nInstance = createInstance();
await i18nInstance
.use(initReactI18next)
.use(
resourcesToBackend((language, namespace) =>
import(`./locales/${language}/${namespace}.json`)
)
)
.init(getOptions(lng, ns));
return i18nInstance;
};
export async function useTranslation(lng, ns, options = {}) {
const i18nextInstance = await initI18next(lng, ns);
return {
t: i18nextInstance.getFixedT(
lng,
Array.isArray(ns) ? ns[0] : ns,
options.keyPrefix
),
i18n: i18nextInstance,
};
}

View File

@ -0,0 +1,43 @@
{
"give me a star in GitHub": " give me a star in GitHub",
"更新索引": "update paper reference index",
"AI写作": "AI writing",
"Paper2AI": "Paper2AI",
"点击AI写作就是正常的对话交流点击寻找文献会根据输入的主题词去寻找对应论文": "Click AI Write for normal conversation, click Paper2AI to find corresponding papers based on the input topic",
"选择论文来源": "Select the source of the paper",
"选择AI模型": "Select AI model",
"生成轮数": "Generation Rounds",
"时间范围": "Range of literature release dates, from this time to this year",
"更新文中的上标,使得数字顺序排列": "Update the superscript in the text to make the numbers in order",
"停止生成": "Stop Generation",
"+ Add Paper": "+ Add Paper",
"Buy VIP TO UNLOCK Cloud Sync and Edit Mutiple Papers Simultaneously": "Buy VIP TO UNLOCK Cloud Sync and Edit Mutiple Papers Simultaneously",
"Paper Management": "Paper Management",
"Your Cloud Papers": "Your Cloud Papers",
"复制": "Copy",
"添加自定义引用": "Add Custom Reference",
"复制所有引用": "Copy All References",
"删除所有引用": "Delete All References",
"Title": "Title",
"Author": "Author",
"Year": "Year",
"Publisher": "Publisher",
"Url": "Url",
"配置选择器": "Configure Selector",
"Upstream URL:": "Upstream URL:",
"System Prompt(Paper2AI):": "System Prompt(Paper2AI):",
"configurations": {
"cocopilot-gpt4": "cocopilot-gpt4 (apiKey prefix with ghu, as GitHub does not allow uploading complete keys)",
"deepseek-chat": "deepseek-chat (Model needs to be manually changed to this one)",
"caifree": "caifree (Recommended)",
"linuxdo": "linuxdo",
"coze": "coze",
"vv佬": "vv giant(Recommended)",
"官网反代": "Official website reverse proxy",
"蒙恬大将军": "Mengtian General(Recommended)",
"oneapi": "oneapi",
"custom": "Custom"
},
"鼠标点击段落中的上标跳转到文献引用?": "Click the superscript in the paragraph to jump to the reference?",
"是否检查文献与主题相关性(如果不相关则不会传给AI引用)": "Check the relevance of the literature to the topic (if it is not relevant, it will not be passed to the AI reference)"
}

View File

@ -0,0 +1,44 @@
{
"give me a star in GitHub": "在GitHub上给我一颗star",
"更新索引": "更新索引",
"AI写作": "AI写作",
"Paper2AI": "寻找文献",
"点击AI写作就是正常的对话交流点击寻找文献会根据输入的主题词去寻找对应论文": "点击AI写作就是正常的对话交流点击寻找文献会根据输入的主题词去寻找对应论文",
"选择论文来源": "选择论文来源",
"选择AI模型": "选择AI模型",
"生成轮数": "生成轮数",
"时间范围": "文献发布日期范围,从这个时间到今年",
"更新文中的上标,使得数字顺序排列": "更新文中的上标,使得数字顺序排列",
"停止生成": "停止生成",
"+ Add Paper": "+ 添加新论文(会直接替换编辑器里的内容)",
"Buy VIP TO UNLOCK Cloud Sync and Edit Mutiple Papers Simultaneously": "购买VIP解锁云同步和同时编辑多篇论文",
"Paper Management": "论文管理",
"Your Cloud Papers": "您的云端论文",
"复制": "复制",
"添加自定义引用": "添加自定义引用",
"复制所有引用": "复制所有引用",
"删除所有引用": "删除所有引用",
"Title": "标题",
"Author": "作者",
"Year": "年份",
"Publisher": "出版商",
"Url": "论文网址",
"配置选择器": "配置选择器",
"Upstream URL:": "请求模型的URL:",
"System Prompt(Paper2AI):": "系统提示(Paper2AI):",
"configurations": {
"cocopilot-gpt4": "cocopilot-gpt4apiKey前面手动加上ghu因为GitHub不允许上传完整的密钥",
"deepseek-chat": "deepseek-chat需要手动修改模型为这个",
"caifree": "caifree推荐",
"linuxdo": "linuxdo",
"coze": "扣子coze(我亲自维护)",
"官网反代": "官网反代",
"vv佬": "vv佬(推荐)",
"蒙恬大将军": "蒙恬大将军(推荐)",
"oneapi": "oneapi",
"custom": "自定义"
},
"鼠标点击段落中的上标跳转到文献引用?": "鼠标点击段落中的上标跳转到文献引用?",
"是否检查文献与主题相关性(如果不相关则不会传给AI引用)": "是否检查文献与主题相关性如果不相关则不会传给AI引用"
}

16
app/i18n/settings.js Normal file
View File

@ -0,0 +1,16 @@
export const fallbackLng = "en";
export const languages = [fallbackLng, "zh-CN"];
export const defaultNS = "translation";
export const cookieName = "i18next";
export function getOptions(lng = fallbackLng, ns = defaultNS) {
return {
// debug: true,
supportedLngs: languages,
fallbackLng,
lng,
fallbackNS: defaultNS,
defaultNS,
ns,
};
}

View File

@ -1,15 +1,49 @@
import { GeistSans } from "geist/font/sans";
import "./globals.css";
import { GoogleAnalytics } from "@next/third-parties/google";
import Script from "next/script";
const defaultUrl = process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}`
: "http://localhost:3000";
export const metadata = {
manifest: "/manifest.json",
metadataBase: new URL(defaultUrl),
title: "paper ai 使用真实文献让AI完成论文",
description: "写论文最高效的方式",
keywords: [
"free AI",
"免费AI模型",
"AI",
"AI paper",
"true references",
"真实文献",
"真实文献引用",
],
authors: [{ name: "liuweiqing", url: "https://github.com/14790897" }],
creator: "liuweiqing",
publisher: "liuweiqing",
alternates: {
canonical: "/",
languages: {
"en-US": "/en-US",
"de-DE": "/de-DE",
},
},
openGraph: {
images:
"https://file.paperai.life/2024/02/540f3476ef43c831934ce0359c367acd.png",
},
twitter: {
card: "page",
title: "AI write",
description: "The fastest way to write paper",
creator: "@hahfrank",
images: [
"https://file.paperai.life/2024/02/540f3476ef43c831934ce0359c367acd.png",
],
},
};
export default function RootLayout({
@ -19,12 +53,57 @@ export default function RootLayout({
}) {
return (
<html lang="en" className={GeistSans.className}>
{/* <Script>{`
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('/service-worker.js').then(function(registration) {
// 注册成功
console.log('ServiceWorker registration successful with scope: ', registration.scope);
}, function(err) {
// 注册失败 :(
console.log('ServiceWorker registration failed: ', err);
});
});
}
`}</Script> */}
{/* msft clarify */}
<Script>
{`(function (c, l, a, r, i, t, y) {
c[a] =
c[a] ||
function () {
(c[a].q = c[a].q || []).push(arguments);
};
t = l.createElement(r);
t.async = 1;
t.src = "https://www.clarity.ms/tag/" + i;
y = l.getElementsByTagName(r)[0];
y.parentNode.insertBefore(t, y);
})(window, document, "clarity", "script", "l869naiex9");`}
</Script>
{/* google一键登录 */}
<Script src="https://accounts.google.com/gsi/client" async></Script>
<body className="bg-background text-foreground">
<main className="min-h-screen flex flex-col items-center">
{children}
</main>
</body>
{/* 谷歌分析 */}
<GoogleAnalytics gaId="G-05DHTG9XQ5" />
{/* vocechat聊天 */}
<Script
data-host-id="1"
data-auto-reg="true"
data-login-token=""
data-theme-color="#3EB489"
data-close-width="48"
data-close-height="48"
data-open-width="380"
data-open-height="480"
data-welcome="欢迎提问"
src="https://voce.paperai.life/widget.js"
async
></Script>
</html>
);
}

View File

@ -1,118 +0,0 @@
import Link from 'next/link'
import { headers, cookies } from 'next/headers'
import { createClient } from '@/utils/supabase/server'
import { redirect } from 'next/navigation'
export default function Login({
searchParams,
}: {
searchParams: { message: string }
}) {
const signIn = async (formData: FormData) => {
'use server'
const email = formData.get('email') as string
const password = formData.get('password') as string
const cookieStore = cookies()
const supabase = createClient(cookieStore)
const { error } = await supabase.auth.signInWithPassword({
email,
password,
})
if (error) {
return redirect('/login?message=Could not authenticate user')
}
return redirect('/')
}
const signUp = async (formData: FormData) => {
'use server'
const origin = headers().get('origin')
const email = formData.get('email') as string
const password = formData.get('password') as string
const cookieStore = cookies()
const supabase = createClient(cookieStore)
const { error } = await supabase.auth.signUp({
email,
password,
options: {
emailRedirectTo: `${origin}/auth/callback`,
},
})
if (error) {
return redirect('/login?message=Could not authenticate user')
}
return redirect('/login?message=Check email to continue sign in process')
}
return (
<div className="flex-1 flex flex-col w-full px-8 sm:max-w-md justify-center gap-2">
<Link
href="/"
className="absolute left-8 top-8 py-2 px-4 rounded-md no-underline text-foreground bg-btn-background hover:bg-btn-background-hover flex items-center group text-sm"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="mr-2 h-4 w-4 transition-transform group-hover:-translate-x-1"
>
<polyline points="15 18 9 12 15 6" />
</svg>{' '}
Back
</Link>
<form
className="animate-in flex-1 flex flex-col w-full justify-center gap-2 text-foreground"
action={signIn}
>
<label className="text-md" htmlFor="email">
Email
</label>
<input
className="rounded-md px-4 py-2 bg-inherit border mb-6"
name="email"
placeholder="you@example.com"
required
/>
<label className="text-md" htmlFor="password">
Password
</label>
<input
className="rounded-md px-4 py-2 bg-inherit border mb-6"
type="password"
name="password"
placeholder="••••••••"
required
/>
<button className="bg-green-700 rounded-md px-4 py-2 text-foreground mb-2">
Sign In
</button>
<button
formAction={signUp}
className="border border-foreground/20 rounded-md px-4 py-2 text-foreground mb-2"
>
Sign Up
</button>
{searchParams?.message && (
<p className="mt-4 p-4 bg-foreground/10 text-foreground text-center">
{searchParams.message}
</p>
)}
</form>
</div>
)
}

View File

@ -1,10 +0,0 @@
import { createClient } from '@/utils/supabase/server';
import { cookies } from 'next/headers';
export default async function Notes() {
const cookieStore = cookies()
const supabase = createClient(cookieStore);
const { data: notes } = await supabase.from("notes").select();
return <pre>{JSON.stringify(notes, null, 2)}</pre>
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 278 KiB

View File

@ -1,74 +0,0 @@
import DeployButton from "../components/DeployButton";
import AuthButton from "../components/AuthButton";
import { createClient } from "@/utils/supabase/server";
import ConnectSupabaseSteps from "@/components/ConnectSupabaseSteps";
import SignUpUserSteps from "@/components/SignUpUserSteps";
import Header from "@/components/Header";
import { cookies } from "next/headers";
import QEditor from "@/components/QuillEditor";
import QuillWrapper from "./QuillWrapper";
// import TinyEditor from "../components/TinyEditor";
// import SEditor from "../components/SlateEditor";
import SettingsLink from "@/components/SettingsLink";
import { ErrorBoundary } from "next/dist/client/components/error-boundary";
// import Error from "@/app/global-error";
export default async function Index() {
const cookieStore = cookies();
const canInitSupabaseClient = () => {
// This function is just for the interactive tutorial.
// Feel free to remove it once you have Supabase connected.
try {
createClient(cookieStore);
return true;
} catch (e) {
return false;
}
};
const isSupabaseConnected = canInitSupabaseClient();
return (
<div className="flex-1 w-full flex flex-col gap-20 items-center">
<nav className="w-full flex justify-center border-b border-b-foreground/10 h-16">
<div className="w-full max-w-4xl flex justify-between items-center p-3 text-sm">
<DeployButton />
{isSupabaseConnected && <AuthButton />}
<SettingsLink />
</div>
</nav>
{/* <ErrorBoundary fallback={<Error />}> */}
<QuillWrapper />
{/* </ErrorBoundary> */}
<footer className="w-full border-t border-t-foreground/10 p-8 flex justify-center text-center text-xs">
<p>
<a
href="https://github.com/14790897/paper-ai"
target="_blank"
className="font-bold hover:underline"
rel="noreferrer"
>
give me a star in GitHub
</a>
</p>
</footer>
</div>
);
}
{
/* <div className="animate-in flex-1 flex flex-col gap-20 opacity-0 max-w-4xl px-3">
<Header />
<main className="flex-1 flex flex-col gap-6">
<h2 className="font-bold text-4xl mb-4">Next steps</h2>
{isSupabaseConnected ? <SignUpUserSteps /> : <ConnectSupabaseSteps />}
</main>
</div> */
}
{
/* <div className="animate-in flex-1 flex flex-col gap-20 opacity-0 max-w-4xl px-3"> */
}
{
/*</div> */
}

5
app/robots.txt Normal file
View File

@ -0,0 +1,5 @@
User-Agent: *
Allow: /
Disallow: /private/
Sitemap: https://www.paperai.life/sitemap.xml

View File

@ -1,13 +0,0 @@
import SettingsWrapper from "../SettingsWrapper";
export default function settings() {
return (
<div className="h-screen w-full flex">
<div className="m-auto">
<h1 className="font-bold text-3xl">settings</h1>
<br />
<SettingsWrapper />
</div>
</div>
);
}

30
app/sitemap.ts Normal file
View File

@ -0,0 +1,30 @@
import { MetadataRoute } from "next";
export default function sitemap(): MetadataRoute.Sitemap {
return [
{
url: "https://www.paperai.life",
lastModified: new Date(),
changeFrequency: "weekly",
priority: 1,
},
{
url: "https://www.paperai.life/settings",
lastModified: new Date(),
changeFrequency: "weekly",
priority: 0.5,
},
{
url: "https://docs.paperai.life",
lastModified: new Date(),
changeFrequency: "weekly",
priority: 0.8,
},
{
url: "https://store.paperai.life",
lastModified: new Date(),
changeFrequency: "weekly",
priority: 0.5,
},
];
}

View File

@ -3,23 +3,46 @@ import { useDispatch, TypedUseSelectorHook, useSelector } from "react-redux";
import { persistReducer } from "redux-persist";
// import storage from "redux-persist/lib/storage";
import { authReducer } from "./slices/authSlice";
import { stateReducer } from "./slices/stateSlice";
import storage from "./customStorage";
import logger from "redux-logger";
const authPersistConfig = {
key: "chatapi",
storage: storage,
whitelist: ["apiKey", "referencesRedux", "editorContent", "upsreamUrl"],
whitelist: [
"apiKey",
"referencesRedux",
"editorContent",
"upsreamUrl",
"systemPrompt",
],
};
const statePersistConfig = {
key: "state1",
storage: storage,
whitelist: [
"showPaperManagement",
"paperNumberRedux",
"contentUpdatedFromNetwork",
"isVip",
"language",
"isJumpToReference",
"isEvaluateTopicMatch",
"citationStyle",
],
};
const rootReducer = combineReducers({
auth: persistReducer(authPersistConfig, authReducer),
state: persistReducer(statePersistConfig, stateReducer),
});
export const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({ serializableCheck: false }).concat(logger),
getDefaultMiddleware({ serializableCheck: false }), //.concat(logger)
});
export type RootState = ReturnType<typeof store.getState>;

View File

@ -5,30 +5,57 @@ export interface APIState {
referencesRedux: Reference[];
editorContent: string;
upsreamUrl: string;
systemPrompt: string;
showPaperManagement: boolean;
}
const initialState: APIState = {
apiKey: "sk-aiHrrRLYUUelHstX69E9484509254dBf92061d6744FfFaD1",
apiKey: "sk-GHuPUV6ERD8wVmmr36FeB8D809D34d93Bb857c009f6aF9Fe", //sk-ffe19ebe9fa44d00884330ff1c18cf82
referencesRedux: [],
editorContent: "",
upsreamUrl: "https://one.caifree.com", //https://api.openai.com
upsreamUrl: "https://one.paperai.life", //https://api.openai.com https://one.caifree.com https://chatserver.3211000.xyz https://api.deepseek.com
systemPrompt: `作为论文写作助手,您的主要任务是根据用户提供的研究主题和上下文,以及相关的研究论文,来撰写和完善学术论文。在撰写过程中,请注意以下要点:
1.使
2.使 [1]***[1]*
3.
4.
5.使
6.
...[1],...[2]`,
showPaperManagement: false,
};
export const authSlice = createSlice({
name: "auth",
initialState,
reducers: {
setShowPaperManagement: (state, action: PayloadAction<boolean>) => {
state.showPaperManagement = action.payload;
},
setApiKey: (state, action: PayloadAction<string>) => {
state.apiKey = action.payload;
},
setUpsreamUrl: (state, action: PayloadAction<string>) => {
state.upsreamUrl = action.payload;
},
setSystemPrompt: (state, action: PayloadAction<string>) => {
state.systemPrompt = action.payload;
},
setEditorContent: (state, action: PayloadAction<string>) => {
state.editorContent = action.payload;
},
addReferenceRedux: (state, action: PayloadAction<Reference>) => {
state.referencesRedux.push(action.payload);
},
addReferencesRedux: (state, action: PayloadAction<Reference[]>) => {
state.referencesRedux.push(...action.payload);
addReferencesRedux: (
state,
action: PayloadAction<{ references: Reference[]; position?: number }>
) => {
const { references, position } = action.payload;
const insertPosition =
position !== undefined ? position : state.referencesRedux.length;
state.referencesRedux.splice(insertPosition, 0, ...references);
},
removeReferenceRedux: (state, action: PayloadAction<number>) => {
state.referencesRedux = state.referencesRedux.filter(
@ -38,14 +65,35 @@ export const authSlice = createSlice({
clearReferencesRedux: (state) => {
state.referencesRedux = [];
},
setEditorContent: (state, action: PayloadAction<string>) => {
state.editorContent = action.payload;
setReferencesRedux: (state, action: PayloadAction<Reference[]>) => {
state.referencesRedux = action.payload;
},
swapReferencesRedux: (
state,
action: PayloadAction<{ indexA: number; indexB: number }>
) => {
console.log("moveReference", state.referencesRedux); // 调试输出
const { indexA, indexB } = action.payload;
if (
indexA >= 0 &&
indexA < state.referencesRedux.length &&
indexB >= 0 &&
indexB < state.referencesRedux.length
) {
const newReferences = [...state.referencesRedux];
const temp = newReferences[indexA];
newReferences[indexA] = newReferences[indexB];
newReferences[indexB] = temp;
state.referencesRedux = newReferences;
}
},
},
});
// Action creators are generated for each case reducer function
export const {
setShowPaperManagement,
setApiKey,
setUpsreamUrl,
addReferenceRedux,
@ -53,6 +101,9 @@ export const {
removeReferenceRedux,
clearReferencesRedux,
setEditorContent,
setReferencesRedux,
setSystemPrompt,
swapReferencesRedux,
} = authSlice.actions;
export const authReducer = authSlice.reducer;

View File

@ -0,0 +1,73 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
export interface APIState {
showPaperManagement: boolean;
paperNumberRedux: string;
contentUpdatedFromNetwork: boolean;
isVip: boolean;
language: string;
isJumpToReference: boolean;
isEvaluateTopicMatch: boolean;
citationStyle: string;
}
const initialState: APIState = {
showPaperManagement: false,
paperNumberRedux: "1", //默认得给个值
contentUpdatedFromNetwork: false,
isVip: false,
language: "en",
isJumpToReference: false,
isEvaluateTopicMatch: false,
citationStyle: "custom-chinese",
};
export const stateSlice = createSlice({
name: "state",
initialState,
reducers: {
setShowPaperManagement: (state) => {
state.showPaperManagement = !state.showPaperManagement;
console.log("state.showPaperManagement", state.showPaperManagement);
},
setPaperNumberRedux: (state, action: PayloadAction<string>) => {
// state.paperNumberRedux = action.payload;
// console.log("state.paperNumberRedux", state.paperNumberRedux);
return {
...state,
paperNumberRedux: action.payload,
};
},
setContentUpdatedFromNetwork: (state, action: PayloadAction<boolean>) => {
state.contentUpdatedFromNetwork = action.payload;
},
setIsVip: (state, action: PayloadAction<boolean>) => {
state.isVip = action.payload;
},
setLanguage: (state, action: PayloadAction<string>) => {
state.language = action.payload;
},
setIsJumpToReference: (state, action: PayloadAction<boolean>) => {
state.isJumpToReference = action.payload;
},
setIsEvaluateTopicMatch: (state, action: PayloadAction<boolean>) => {
state.isEvaluateTopicMatch = action.payload;
},
setCitationStyle: (state, action: PayloadAction<string>) => {
state.citationStyle = action.payload;
},
},
});
// Action creators are generated for each case reducer function
export const {
setShowPaperManagement,
setPaperNumberRedux,
setContentUpdatedFromNetwork,
setIsVip,
setLanguage,
setIsJumpToReference,
setIsEvaluateTopicMatch,
setCitationStyle,
} = stateSlice.actions;
export const stateReducer = stateSlice.reducer;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 278 KiB

View File

@ -1,28 +1,36 @@
import { createClient } from '@/utils/supabase/server'
import Link from 'next/link'
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
import { createClient } from "@/utils/supabase/server";
import Link from "next/link";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { insertUserProfile } from "@/utils/supabase/supabaseutils";
export default async function AuthButton() {
const cookieStore = cookies()
const supabase = createClient(cookieStore)
const cookieStore = cookies();
const supabase = createClient(cookieStore);
const {
data,
data: { user },
} = await supabase.auth.getUser()
} = await supabase.auth.getUser();
//profiles表 插入用户信息
await insertUserProfile(data, supabase);
// console.log("1111 in AuthButton user:", user);
const signOut = async () => {
'use server'
"use server";
const cookieStore = cookies()
const supabase = createClient(cookieStore)
await supabase.auth.signOut()
return redirect('/login')
}
const cookieStore = cookies();
const supabase = createClient(cookieStore);
await supabase.auth.signOut();
return redirect("/login");
};
return user ? (
<div className="flex items-center gap-4">
Hey, {user.email}!
{/* <div className="vip-icon bg-yellow-400 text-white p-2 rounded-full shadow-lg animate-pulse">
VIP
</div> */}
<form action={signOut}>
<button className="py-2 px-4 rounded-md no-underline bg-btn-background hover:bg-btn-background-hover">
Logout
@ -36,5 +44,5 @@ export default async function AuthButton() {
>
Login
</Link>
)
);
}

View File

@ -0,0 +1,27 @@
import React from "react";
import { sendGAEvent } from "@next/third-parties/google";
//i18n
import { useTranslation } from "@/app/i18n/client";
// BuyVipButton 组件
function BuyVipButton({ lng }: { lng: string }) {
//i18n
const { t } = useTranslation(lng);
// 这是购买VIP的目标URL
const targetUrl = "https://store.paperai.life";
return (
<a href={targetUrl} target="_blank" className="no-underline">
<button
className="bg-gold text-white font-semibold text-lg py-2 px-4 rounded cursor-pointer border-none shadow-md transition duration-300 ease-in-out transform hover:scale-110"
onClick={() =>
sendGAEvent({ event: "buyVipButtonClicked", value: "buy vip" })
}
>
{t(
"Buy VIP TO UNLOCK Cloud Sync and Edit Mutiple Papers Simultaneously"
)}
</button>
</a>
);
}
export default BuyVipButton;

View File

@ -2,7 +2,7 @@ export default function DeployButton() {
return (
<a
className="py-2 px-3 flex rounded-md no-underline hover:bg-btn-background-hover border"
href="https://vercel.com/new/clone?repository-url=https://github.com/14790897/paper-ai&project-name=paper-ai&repository-name=paper-ai&demo-title=paper-ai&demo-description=This%20starter%20configures%20Supabase%20Auth%20to%20use%20cookies%2C%20making%20the%20user's%20session%20available%20throughout%20the%20entire%20Next.js%20app%20-%20Client%20Components%2C%20Server%20Components%2C%20Route%20Handlers%2C%20Server%20Actions%20and%20Middleware.&demo-url=https%3A%2F%2Fdemo-nextjs-with-supabase.vercel.app%2F&external-id=https%3A%2F%2Fgithub.com%2Fvercel%2Fnext.js%2Ftree%2Fcanary%2Fexamples%2Fwith-supabase&demo-image=https%3A%2F%2Fpaperai.life%2Fopengraph-image.png"
href="https://vercel.com/new/clone?repository-url=https://github.com/14790897/paper-ai&project-name=paper-ai&repository-name=paper-ai&demo-title=paper-ai&demo-description=This%20starter%20configures%20Supabase%20Auth%20to%20use%20cookies%2C%20making%20the%20user's%20session%20available%20throughout%20the%20entire%20Next.js%20app%20-%20Client%20Components%2C%20Server%20Components%2C%20Route%20Handlers%2C%20Server%20Actions%20and%20Middleware.&demo-url=https%3A%2F%2Fdemo-nextjs-with-supabase.vercel.app%2F&external-id=https%3A%2F%2Fgithub.com%2Fvercel%2Fnext.js%2Ftree%2Fcanary%2Fexamples%2Fwith-supabase&demo-image=https://file.paperai.life/2024/02/540f3476ef43c831934ce0359c367acd.png"
target="_blank"
rel="noreferrer"
>

90
components/Export.tsx Normal file
View File

@ -0,0 +1,90 @@
"use client";
import { useCallback } from "react";
import { saveAs } from "file-saver";
import * as quillToWord from "quill-to-word";
//redux
import { useAppDispatch, useAppSelector } from "@/app/store";
import ReduxProvider from "@/app/store/ReduxProvider";
import { Reference } from "@/utils/global";
import { getAllFullReferences } from "@/utils/others/quillutils";
type ParaIn = {
editor: any;
};
const ExportDocx = ({ editor }: ParaIn) => {
const references = useAppSelector((state) => state.auth.referencesRedux);
const citationStyle = useAppSelector((state) => state.state.citationStyle);
const prepareReferencesForQuill = (references: Reference[]) => {
// 首先添加一个标题
const referencesWithTitle = [
{
attributes: {
bold: true,
align: "center",
},
insert: "\n参考文献\n",
},
];
const referencesString = getAllFullReferences(references, citationStyle);
const quillReferences = [
{
attributes: {
// 提供默认值,即使这些值不会改变文本样式
bold: false, // 默认为false因为引用通常不需要加粗
align: "left", // 默认为left这是大多数文本的常规对齐方式
},
insert: referencesString,
},
];
// 合并标题和引用列表
return referencesWithTitle.concat(quillReferences);
};
const exportToWord = useCallback(async () => {
console.log(editor);
if (!editor) {
console.error("Editor is not initialized yet");
return;
}
// 准备引用内容
const quillReferences = prepareReferencesForQuill(references);
// 获取当前编辑器内容
let editorContents = editor.getContents();
// 添加引用到编辑器内容的末尾
quillReferences.forEach((reference) => {
editorContents.ops.push(reference);
});
// editor.updateContents({
// ops: quillReferences,
// });
console.log("editorContents", editorContents);
const quillToWordConfig = {
exportAs: "blob",
};
const docAsBlob = await quillToWord.generateWord(
editorContents,
quillToWordConfig
);
saveAs(docAsBlob, "word-export.docx");
}, [editor, references]);
return (
<ReduxProvider>
<div className="flex justify-center items-center">
<button
onClick={exportToWord}
className="px-4 py-2 text-white bg-blue-500 hover:bg-blue-600 rounded shadow"
>
Export to Word
</button>
</div>
</ReduxProvider>
);
};
export default ExportDocx;

View File

@ -0,0 +1,25 @@
import Link from "next/link";
import { Trans } from "react-i18next/TransWithoutContext";
import { languages } from "@/app/i18n/settings";
export const FooterBase = ({ t, lng }) => {
return (
<div>
{/* <footer style={{ marginTop: 50 }}> */}
<Trans i18nKey="languageSwitcher" t={t}>
Language Manager:Switch from <strong>{{ lng }}</strong> to:{" "}
</Trans>
{languages
.filter((l) => lng !== l)
.map((l, index) => {
return (
<span key={l}>
{index > 0 && " or "}
<Link href={`/${l}`}>{l}</Link>
</span>
);
})}
{/* </footer> */}
</div>
);
};

View File

View File

@ -0,0 +1,25 @@
import Link from "next/link";
import { Trans } from "react-i18next/TransWithoutContext";
import { languages } from "../../../i18n/settings";
import { useTranslation } from "../../../i18n";
export const Footer = async ({ lng }) => {
const { t } = await useTranslation(lng, "footer");
return (
<footer style={{ marginTop: 50 }}>
<Trans i18nKey="languageSwitcher" t={t}>
Switch from <strong>{{ lng }}</strong> to:{" "}
</Trans>
{languages
.filter((l) => lng !== l)
.map((l, index) => {
return (
<span key={l}>
{index > 0 && " or "}
<Link href={`/${l}`}>{l}</Link>
</span>
);
})}
</footer>
);
};

View File

@ -37,13 +37,15 @@ interface Author {
async function getArxivPapers(
query: string,
maxResults = 5,
maxResults = 2,
offset = -1,
sortBy = "submittedDate",
sortOrder = "descending"
) {
const maxOffset = 30 - maxResults; // 假设总记录数为 100
const start = getRandomOffset(maxOffset);
const url = `https://export.arxiv.org/api/query?search_query=${query}&start=${start}&max_results=${maxResults}&sortBy=${sortBy}&sortOrder=${sortOrder}`;
const maxOffset = 30 - maxResults; // 假设总记录数为 20
if (offset === -1) offset = getRandomOffset(maxOffset);
console.log("offset in arxiv", offset);
const url = `https://export.arxiv.org/api/query?search_query=${query}&start=${offset}&max_results=${maxResults}&sortBy=${sortBy}&sortOrder=${sortOrder}`;
try {
const response = await axios.get(url);
@ -53,18 +55,15 @@ async function getArxivPapers(
// 你可以在这里处理数据
result = extractArxivData(result);
return result;
} catch (error) {
if (error.response) {
// 请求已发送,但服务器响应的状态码不在 2xx 范围内
console.error("Error fetching data: ", error.response.data);
} else if (error.request) {
// 请求已发送,但没有收到响应
console.error("No response received: ", error.request);
} else {
// 发送请求时出现错误
console.error("Error setting up the request: ", error.message);
}
return null;
} catch (error: any) {
throw new Error(
`Arxiv失败请使用英文并缩短关键词:${JSON.stringify(
error.response,
null,
2
)}`
);
// return null;
}
}
@ -77,7 +76,7 @@ function extractArxivData(data: ArxivFeed) {
id: entry.id[0],
published: entry.published[0],
title: entry.title[0],
summary: entry.summary[0],
abstract: entry.summary[0],
authors: entry.author.map((author) => author.name[0]),
};
});

View File

@ -10,14 +10,20 @@ type PubMedID = string;
// 定义idList为PubMedID数组
type IDList = PubMedID[];
async function getPubMedPapers(query: string, year: number, limit = 2) {
async function getPubMedPapers(
query: string,
year: number,
offset = -1,
limit = 2
) {
try {
const baseURL =
"https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi";
const db = "pubmed"; // 设定搜索的数据库为PubMed
const retMax = limit; // 检索的最大记录数
const retStart = getRandomOffset(30 - limit); // 假设每页最多30条根据需要随机偏移
const url = `${baseURL}?db=${db}&term=${query}[Title/Abstract]+AND+2018:3000[Date - Publication]&retMax=${retMax}&retStart=${retStart}&api_key=${process.env.NEXT_PUBLIC_PUBMED_API_KEY}`;
const maxOffset = 20 - limit; // 假设总记录数为 20
if (offset === -1) offset = getRandomOffset(maxOffset);
const url = `${baseURL}?db=${db}&term=${query}[Title/Abstract]+AND+${year}:3000[Date - Publication]&retMax=${retMax}&retStart=${offset}&api_key=${process.env.NEXT_PUBLIC_PUBMED_API_KEY}`;
const response = await axios.get(url, { responseType: "text" });
console.log(response.data);
// 解析XML数据
@ -31,7 +37,7 @@ async function getPubMedPapers(query: string, year: number, limit = 2) {
// 这里只返回了ID列表你可能需要根据实际需要进行调整
return idList;
} catch (error) {
console.error("Error fetching data from PubMed API:", error);
console.error(" PubMed API失败(请使用英文并缩短关键词):", error);
return null; // 或根据需要处理错误
}
}
@ -53,25 +59,25 @@ async function getPubMedPaperDetails(idList: IDList) {
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
throw new Error(`${response.text()}`);
}
const data = await response.text(); // 获取响应文本
// 解析XML数据
const parser = new xml2js.Parser({
explicitArray: false,
ignoreAttrs: true, // 忽略XML属性
ignoreAttrs: false, // 忽略XML属性
charkey: "text", // 字符数据的键
trim: true, // 去除文本前后空格
});
let result = await parser.parseStringPromise(data);
console.log(result);
// console.log(result);
// 提取并处理文章详细信息
const articles = result.PubmedArticleSet.PubmedArticle.map((article) => {
const medlineCitation = article.MedlineCitation;
const articleDetails = medlineCitation.Article;
// console.log("atricledetails", articleDetails);
const abstractTexts = articleDetails.Abstract.AbstractText;
let abstract;
@ -79,7 +85,7 @@ async function getPubMedPaperDetails(idList: IDList) {
if (Array.isArray(abstractTexts)) {
// 如果是数组,遍历数组并连接每个元素的文本
abstract = abstractTexts
.map((text) => (typeof text === "object" ? text._ : text))
.map((text) => (typeof text === "object" ? text.text : text))
.join(" ");
} else if (typeof abstractTexts === "string") {
// 如果 abstractTexts 直接就是字符串
@ -106,48 +112,92 @@ async function getPubMedPaperDetails(idList: IDList) {
return names.join(" ");
})
: ["Unknown Author"];
const journalTitle = articleDetails.Journal.Title; // 提取出版者信息(杂志标题)
let publishedDate = "No date available";
// 尝试从 ArticleDate 获取发表日期
if (articleDetails.ArticleDate) {
publishedDate = `${articleDetails.ArticleDate.Year}-${articleDetails.ArticleDate.Month}-${articleDetails.ArticleDate.Day}`;
publishedDate = `${articleDetails.ArticleDate.Year}`;
}
// 如果 ArticleDate 不存在,尝试从 JournalIssue/PubDate 获取
else if (articleDetails.Journal.JournalIssue.PubDate) {
publishedDate = `${articleDetails.Journal.JournalIssue.PubDate.Year}-${
articleDetails.Journal.JournalIssue.PubDate.Month || ""
publishedDate = `${articleDetails.Journal.JournalIssue.PubDate.Year}
}`;
}
let journalTitle = articleDetails.Journal.Title; // 提取出版者信息(杂志标题)
journalTitle += `, ${publishedDate}`;
if (articleDetails.Journal.JournalIssue.Volume) {
journalTitle += `, ${articleDetails.Journal.JournalIssue.Volume}`;
}
if (articleDetails.Pagination) {
journalTitle += `: ${articleDetails.Pagination.StartPage}-${articleDetails.Pagination.EndPage}`;
}
// 构建文章的 PubMed URL
const articleUrl = `https://pubmed.ncbi.nlm.nih.gov/${medlineCitation.PMID._}/`;
const articleUrl = `https://pubmed.ncbi.nlm.nih.gov/${medlineCitation.PMID.text}/`;
// console.log("medlineCitation", medlineCitation);
console.log("\n,journalTitle", journalTitle);
let title = articleDetails.ArticleTitle;
// 检查并去除字符串最后的句点
if (title.endsWith(".")) {
title = title.slice(0, -1);
}
// 提取DOI
let doi = null;
if (
article.PubmedData &&
article.PubmedData.ArticleIdList &&
Array.isArray(article.PubmedData.ArticleIdList.ArticleId)
) {
const doiObject = article.PubmedData.ArticleIdList.ArticleId.find(
(idObj) => idObj.$.IdType === "doi"
);
if (doiObject) {
doi = doiObject.text; // 获取DOI值
}
}
console.log("doi", doi);
console.log(
"链接",
medlineCitation.PMID.text,
"属性",
typeof medlineCitation.PMID.text
);
return {
id: medlineCitation.PMID._,
title: articleDetails.ArticleTitle,
id: Number(medlineCitation.PMID.text),
title: title,
abstract: abstract,
authors: authors,
url: articleUrl,
year: publishedDate,
journal: journalTitle,
doi: doi,
// 其他需要的字段可以继续添加
};
});
return articles;
} catch (error) {
console.error("Error fetching paper details from PubMed:", error);
return null;
throw new Error(`Error fetching paper details from PubMed:", ${error}`);
}
}
// 示例:使用这些函数
async function fetchPubMedData(query: string, year: number, limit: number) {
const idList = await getPubMedPapers(query, year, limit);
if (idList && idList.length > 0) {
const paperDetails = await getPubMedPaperDetails(idList);
console.log("fetchPubMedData", paperDetails); // 处理或显示文章详情
return paperDetails;
async function fetchPubMedData(
query: string,
year: number,
offset: number,
limit: number
) {
try {
const idList = await getPubMedPapers(query, year, offset, limit);
if (idList && idList.length > 0) {
const paperDetails = await getPubMedPaperDetails(idList);
console.log("fetchPubMedData", paperDetails); // 处理或显示文章详情
return paperDetails;
}
} catch (error) {
//这里无法起作用因为pubmed不会返回400系错误
throw new Error(`pubmed: ${error}`);
}
}

View File

@ -1,5 +1,5 @@
import axios from "axios";
import {getRandomOffset} from "@/utils/others/quillutils"
import { getRandomOffset } from "@/utils/others/quillutils";
interface Author {
authorId: string;
name: string;
@ -15,25 +15,31 @@ interface Paper {
url: string;
}
async function getSemanticPapers(query: string, year: string, limit = 2) {
async function getSemanticPapers(
query: string,
year: string,
offset = -1,
limit = 2
) {
try {
const maxOffset = 30 - limit; // 假设总记录数为 100
const offset = getRandomOffset(maxOffset);
const url = `https://api.semanticscholar.org/graph/v1/paper/search`;
const maxOffset = 20 - limit; // 假设总记录数为 20
if (offset === -1) offset = getRandomOffset(maxOffset);
const url = `https://proxy.paperai.life/proxy/https://api.semanticscholar.org/graph/v1/paper/search`;
const response = await axios.get(url, {
headers: {
'x-api-key': process.env.NEXT_PUBLIC_SEMANTIC_API_KEY,
"x-api-key": process.env.NEXT_PUBLIC_SEMANTIC_API_KEY,
},
params: {
query: query,
offset: offset,
limit: 2,
limit: limit,
year: year,
fields: "title,year,authors.name,abstract,venue,url,journal",
fields:
"title,year,authors.name,abstract,venue,url,journal,externalIds",
},
});
// 提取并处理论文数据
const papers = response.data.data.map((paper:Paper) => {
const papers = response.data.data.map((paper: Paper) => {
// 提取每篇论文的作者名字
const authorNames = paper.authors.map((author) => author.name);
@ -43,13 +49,19 @@ async function getSemanticPapers(query: string, year: string, limit = 2) {
};
});
return papers;
} catch (error) {
console.error("Error fetching data from Semantic Scholar API:", error);
return null; // 或根据需要处理错误
} catch (error: any) {
// console.error("Error fetching data from Semantic Scholar API:", error);
throw new Error(
`Semantic Scholar fail请使用英文并缩短关键词:${JSON.stringify(
error.response,
null,
2
)}`
);
// return null;
}
}
// 调用函数示例
// fetchSemanticPapers("covid", 50, 2, "2015-2023").then((data) => {
// console.log(data);

View File

@ -0,0 +1,93 @@
"use client";
import React, { useEffect } from "react";
//supabase
import { createClient } from "@/utils/supabase/client";
const GoogleSignIn = () => {
const supabase = createClient();
// 加载Google身份验证库并初始化
useEffect(() => {
// 异步检查用户是否已经登录
const checkSession = async () => {
const session = await supabase.auth.getSession();
if (session) {
console.log("用户已登录", session);
} else {
loadGoogleSignInScript();
}
};
checkSession();
}, []);
const loadGoogleSignInScript = () => {
// 确保gapi脚本只被加载一次
if (!window.gapi) {
const script = document.createElement("script");
script.src = "https://accounts.google.com/gsi/client";
script.async = true;
script.defer = true;
script.onload = initGoogleSignIn;
document.body.appendChild(script);
} else {
initGoogleSignIn();
}
};
// 初始化Google登录
const initGoogleSignIn = () => {
window.google.accounts.id.initialize({
client_id:
"646783243018-m2n9qfo12k70debpmkesevt5j2hi2itb.apps.googleusercontent.com", // 替换为你的客户端ID
callback: handleSignInWithGoogle,
auto_select: false, // 根据需要设置
itp_support: true,
});
};
// 处理登录响应
const handleSignInWithGoogle = async (response) => {
const { data, error } = await supabase.auth.signInWithIdToken({
provider: "google",
token: response.credential,
nonce: "", // 如果你使用nonce请在这里设置
});
if (error) {
console.error("Login failed:", error);
return;
}
console.log("Login successful:", data);
// 在这里处理登录成功后的逻辑
};
return (
<div>
{/* <div
id="g_id_onload"
data-client_id="646783243018-m2n9qfo12k70debpmkesevt5j2hi2itb.apps.googleusercontent.com"
data-context="signin"
data-ux_mode="popup"
// data-callback="handleSignInWithGoogleccounts.id.ini"
data-nonce=""
data-auto_select="false"
data-itp_support="true"
></div> */}
<div
id="g_id_signin"
className="g_id_signin"
data-type="standard"
data-shape="pill"
data-theme="outline"
data-text="signin_with"
data-size="large"
data-logo_alignment="left"
></div>
</div>
);
};
export default GoogleSignIn;

View File

@ -0,0 +1,31 @@
"use client";
// import React from "react";
const LinuxdoSignin = () => {
console.log(
"process.env.NEXT_PUBLIC_CLIENT_ID",
process.env.NEXT_PUBLIC_CLIENT_ID
);
const handleLogin = () => {
// 构建授权URL
const responseType = "code";
const authUrl = `https://connect.linux.do/oauth2/authorize?response_type=${responseType}&client_id=${process.env.NEXT_PUBLIC_CLIENT_ID}&state=ttt1`;
// 重定向到授权页面
window.location.href = authUrl;
};
return (
<div>
<button
onClick={handleLogin}
className="bg-gradient-to-r from-yellow-400 to-yellow-500 text-white rounded-md px-4 py-2 mb-2 flex items-center justify-center gap-2 hover:from-yellow-500 hover:to-yellow-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-yellow-400 shadow-lg hover:shadow-xl transition ease-in duration-200 w-full
"
>
Login with Linuxdo(free VIP)
</button>
</div>
);
};
export default LinuxdoSignin;

View File

@ -0,0 +1,13 @@
// @/components/LoadingIndicator.tsx
import React from "react";
function LoadingIndicator() {
return (
<div className="flex justify-center items-center p-5">
<div className="spinner animate-spin rounded-full h-8 w-8 border-4 border-blue-500 border-t-transparent"></div>
<p className="text-lg">Loading editor...</p>
</div>
);
}
export default LoadingIndicator;

View File

@ -0,0 +1,34 @@
import React, { useState } from "react";
import { toast } from "react-toastify";
// 自定义Toast内容组件
const ExpandableToastContent = ({ fullText }) => {
const [isExpanded, setIsExpanded] = useState(false);
const toggleExpand = () => setIsExpanded(!isExpanded);
return (
<div className="w-full max-w-none p-4 bg-white rounded-lg shadow dark:bg-gray-800">
{/* 可以继续添加更多的Tailwind CSS类来定制外观 */}
<div className="text-sm text-gray-500 dark:text-gray-400">
{isExpanded ? fullText : `${fullText.substring(0, 100)}...`}
</div>
<button
onClick={toggleExpand}
className="mt-2 text-blue-500 hover:text-blue-600 dark:hover:text-blue-400"
>
{isExpanded ? "Show Less" : "Show More"}
</button>
</div>
);
};
// 使用自定义Toast内容的函数
export const showExpandableToast = (message: string) => {
toast(<ExpandableToastContent fullText={message} />, {
position: "top-center",
autoClose: 3000,
pauseOnHover: true,
className: "toastDetail",
});
};

View File

@ -0,0 +1,32 @@
"use client";
import React from "react";
import { useAppDispatch } from "@/app/store";
import { setShowPaperManagement } from "@/app/store/slices/stateSlice";
export default function PaperListButton() {
const dispatch = useAppDispatch();
const handleClick = () => {
dispatch(setShowPaperManagement());
};
return (
<div
className="py-2 px-3 flex rounded-md no-underline hover:bg-btn-background-hover border cursor-pointer"
onClick={handleClick}
>
<svg
aria-label="Menu"
role="img"
viewBox="0 0 100 80"
className="h-4 w-4 mr-2"
fill="currentColor"
>
<rect width="100" height="20"></rect>
<rect y="30" width="100" height="20"></rect>
<rect y="60" width="100" height="20"></rect>
</svg>
</div>
);
}
// "https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fnext.js%2Ftree%2Fcanary%2Fexamples%2Fwith-supabase&project-name=nextjs-with-supabase&repository-name=nextjs-with-supabase&demo-title=nextjs-with-supabase&demo-description=This%20starter%20configures%20Supabase%20Auth%20to%20use%20cookies%2C%20making%20the%20user's%20session%20available%20throughout%20the%20entire%20Next.js%20app%20-%20Client%20Components%2C%20Server%20Components%2C%20Route%20Handlers%2C%20Server%20Actions%20and%20Middleware.&demo-url=https%3A%2F%2Fdemo-nextjs-with-supabase.vercel.app%2F&external-id=https%3A%2F%2Fgithub.com%2Fvercel%2Fnext.js%2Ftree%2Fcanary%2Fexamples%2Fwith-supabase&demo-image=https%3A%2F%2Fdemo-nextjs-with-supabase.vercel.app%2Fopengraph-image.png&integration-ids=oac_VqOgBHqhEoFTPzGkPd7L0iH6";

View File

@ -0,0 +1,12 @@
"use client";
import ReduxProvider from "@/app/store/ReduxProvider";
import PaperListButton from "@/components/PaperListButton";
export default function PaperListButtonWrapper() {
return (
<ReduxProvider>
<PaperListButton />
</ReduxProvider>
);
}

View File

@ -0,0 +1,251 @@
"use client";
import { useCallback, useState, useRef, useEffect } from "react";
//redux
import { useAppDispatch, useAppSelector } from "@/app/store";
import {
setEditorContent,
setReferencesRedux,
} from "@/app/store/slices/authSlice";
import {
setPaperNumberRedux,
setContentUpdatedFromNetwork,
setIsVip,
setShowPaperManagement,
} from "@/app/store/slices/stateSlice";
//supabase
import { createClient } from "@/utils/supabase/client";
import {
getUser,
getUserPaperNumbers,
getUserPaper,
submitPaper,
deletePaper,
fetchUserVipStatus,
} from "@/utils/supabase/supabaseutils";
//动画
import { CSSTransition } from "react-transition-group";
// import { animated, useSpring } from "@react-spring/web";
//删除远程论文按钮
import ParagraphDeleteButton from "@/components/ParagraphDeleteInterface";
//vip充值按钮
import BuyVipButton from "@/components/BuyVipButton"; // 假设这是购买VIP的按钮组件
//i18n
import { useTranslation } from "@/app/i18n/client";
const PaperManagement = ({ lng }) => {
//i18n
const { t } = useTranslation(lng);
//supabase
const supabase = createClient();
//redux
const dispatch = useAppDispatch();
const paperNumberRedux = useAppSelector(
(state) => state.state.paperNumberRedux
);
const showPaperManagement = useAppSelector(
(state) => state.state.showPaperManagement
);
const editorContent = useAppSelector((state) => state.auth.editorContent);
const referencesRedux = useAppSelector((state) => state.auth.referencesRedux);
//vip状态
const isVip = useAppSelector((state) => state.state.isVip);
//获取的论文数量列表状态
const [paperNumbers, setPaperNumbers] = useState<string[]>([]);
//user id的状态设置
const [userId, setUserId] = useState<string>("");
//获取用户存储在云端的论文使用useCallback定义一个记忆化的函数来获取用户论文
const fetchPapers = useCallback(async () => {
const user = await getUser();
if (user && user.id) {
// console.log("user.id", user.id);
const numbers = await getUserPaperNumbers(user.id, supabase);
setPaperNumbers(numbers || []); // 直接在这里更新状态
setUserId(user.id);
}
}, [supabase]); // 依赖项数组中包含supabase因为它可能会影响到fetchPapers函数的结果
//获取用户VIP状态
const initFetchVipStatue = useCallback(async () => {
const user = await getUser();
if (user && user.id) {
const isVip = await fetchUserVipStatus(user.id);
return isVip;
}
}, [supabase]);
// 使用useEffect在组件挂载后立即获取数据
useEffect(() => {
const checkAndFetchPapers = async () => {
const isVip = await initFetchVipStatue();
dispatch(setIsVip(isVip));
console.log("isVip in initFetchVipStatue", isVip);
if (isVip) {
fetchPapers();
}
};
checkAndFetchPapers();
}, [supabase]);
const handlePaperClick = async (paperNumber: string) => {
const data = await getUserPaper(userId, paperNumber, supabase); // 假设这个函数异步获取论文内容
if (!data) {
throw new Error("查询出错");
}
console.log("paperNumber", paperNumber);
// 更新状态以反映选中的论文内容
dispatch(setEditorContent(data.paper_content)); // 更新 Redux store
dispatch(setReferencesRedux(JSON.parse(data.paper_reference))); // 清空引用列表
dispatch(setPaperNumberRedux(paperNumber)); // 更新当前论文编号
//从网络请求中更新editorContent时同时设置contentUpdatedFromNetwork为true
dispatch(setContentUpdatedFromNetwork(true)); // 更新 Redux store
};
function getNextPaperNumber(paperNumbers: string[]) {
if (paperNumbers.length === 0) {
return "1";
} else {
return String(Math.max(...paperNumbers.map(Number)) + 1);
}
}
const handleAddPaperClick = async () => {
// 先手动保存本地内容到云端
// await submitPaper(
// supabase,
// editorContent,
// referencesRedux,
// paperNumberRedux
// );
// 添加一个新的空白论文
await submitPaper(
supabase,
"This is a blank page",
[],
getNextPaperNumber(paperNumbers)
);
// 重新获取论文列表
await fetchPapers();
};
// const animations = useSpring({
// opacity: showPaperManagement ? 1 : 0,
// from: { opacity: 0 },
// });
//用于判断点击有没有落在区域中
const paperManagementRef = useRef(null); // 用于引用PaperManagement组件的根元素
const handleClickOutside = (event) => {
if (
paperManagementRef.current &&
!paperManagementRef.current.contains(event.target) &&
showPaperManagement
) {
// 如果点击事件的目标不是PaperManagement组件内的元素
// 隐藏组件
console.log("Clicked outside of the PaperManagement component.");
dispatch(setShowPaperManagement());
}
};
useEffect(() => {
if (showPaperManagement) {
// 只有当组件可见时,才添加事件监听器
document.addEventListener("mousedown", handleClickOutside);
}
// 组件卸载或状态改变时移除事件监听器
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [showPaperManagement]); // 依赖项数组包含showPaperManagement状态
return (
<CSSTransition
in={showPaperManagement}
timeout={2000}
classNames="slide"
unmountOnExit
>
{/* showPaperManagement ? ( */}
{/* <animated.div style={animations}> */}
<>
<div
ref={paperManagementRef}
className="paper-management-container flex flex-col items-center space-y-4"
>
<div className="max-w-md w-full bg-blue-gray-100 rounded overflow-hidden shadow-lg mx-auto p-5">
<h1 className="font-bold text-3xl text-center">
{" "}
{t("Paper Management")}
</h1>
</div>
{isVip ? (
<div>
<button
onClick={handleAddPaperClick}
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
>
{t("+ Add Paper")}
</button>
<div className="flex flex-col items-center space-y-2">
<h2 className="text-xl font-semibold">
{" "}
{t("Your Cloud Papers")}
</h2>
{paperNumbers.length > 0 ? (
<ul className="list-disc">
{[...paperNumbers]
.sort((a, b) => parseInt(a, 10) - parseInt(b, 10))
.map((number, index) => (
<li
key={index}
className={`bg-white w-full max-w-md mx-auto rounded shadow p-4 cursor-pointer ${
number === paperNumberRedux ? "bg-yellow-200" : ""
}`}
onClick={() => handlePaperClick(number)}
>
<span>Paper {number}</span>
<ParagraphDeleteButton
index={index}
removeReferenceUpdateIndex={async () => {
await deletePaper(supabase, userId, number);
const numbers = await getUserPaperNumbers(
userId,
supabase
);
setPaperNumbers(numbers || []); // 直接在这里更新状态
}}
isRemovePaper={true}
title="Do you want to delete this paper?"
text="This action cannot be undone"
></ParagraphDeleteButton>
{/* <input
type="text"
value={paper.title}
onChange={(e) => handleTitleChange(index, e.target.value)}
placeholder="Enter paper title"
className="mt-2 p-2 border rounded"
/> */}
</li>
))}
</ul>
) : (
<p>No papers found.</p>
)}
</div>
</div>
) : (
<BuyVipButton lng={lng} />
)}
</div>
</>
{/* </animated.div>
) : null */}
</CSSTransition>
);
};
export default PaperManagement;

View File

@ -0,0 +1,12 @@
"use client";
import ReduxProvider from "@/app/store/ReduxProvider";
import PaperManagement from "@/components/PaperManagement";
export default function PaperManagementWrapper({ lng }) {
return (
<ReduxProvider>
<PaperManagement lng={lng} />
</ReduxProvider>
);
}

View File

@ -0,0 +1,52 @@
import { faL } from "@fortawesome/free-solid-svg-icons";
import React from "react";
import Swal from "sweetalert2";
// 定义Props类型
interface SweetAlertComponentProps {
index: number;
removeReferenceUpdateIndex: (index: number, rmPg: boolean) => void;
}
const ParagraphDeleteButton: React.FC<any> = ({
index,
removeReferenceUpdateIndex,
isRemovePaper = false,
title = "需要同时删除与文献相关的整个段落吗?",
text = "根据周围的换行符来判断是否是同一个段落",
}) => {
//这里传递函数的时候应该把参数先提前弄好 2.7
const showAlert = async () => {
const result = await Swal.fire({
title: title,
text: text,
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#3085d6",
cancelButtonColor: "#d33",
confirmButtonText: "Yes",
});
if (result.isConfirmed) {
if (isRemovePaper) {
removeReferenceUpdateIndex(index, true);
} else {
removeReferenceUpdateIndex();
}
// Swal.fire("Deleted!", "Your file has been deleted.", "success");
} else {
if (isRemovePaper) removeReferenceUpdateIndex(index, false);
// Swal.fire("Cancelled", "Your imaginary file is safe :)", "error");
}
};
return (
<button
className="text-red-500 hover:text-red-700 ml-4"
onClick={showAlert} // 直接使用showAlert而不传递参数
>
X
</button>
);
};
export default ParagraphDeleteButton;

View File

@ -0,0 +1,38 @@
import React from "react";
// 定义props的类型
interface ProgressDisplayProps {
generatedPaperNumber: number;
i: number;
}
// 使用接口为函数组件的props提供类型注解
const ProgressDisplay: React.FC<ProgressDisplayProps> = ({
generatedPaperNumber,
i,
}) => {
// 计算完成的百分比
const percentage = ((i / generatedPaperNumber) * 100).toFixed(2);
return (
<div className="relative">
{/* 可以添加一个进度条来直观显示进度 */}
<div className="h-4 bg-gray-200 rounded-full">
<div
className={`h-full rounded-full ${
Number(percentage) < 100 ? "bg-yellow-500" : "bg-green-500"
}`}
style={{ width: `${percentage}%` }}
></div>
</div>
{/* 文字放置在进度条内部 */}
<div className="absolute inset-0 flex items-center justify-center text-xs text-white pointer-events-none">
<p>
{i} / {generatedPaperNumber}task {percentage}% Complete
</p>
</div>
</div>
);
};
export default ProgressDisplay;

View File

@ -1,31 +1,55 @@
"use client";
import React, { useState, useEffect, useRef } from "react";
import React, { useState, useEffect, useRef, use } from "react";
import Quill from "quill";
import "quill/dist/quill.snow.css";
import { useLocalStorage } from "react-use";
import Link from "next/link";
import * as Sentry from "@sentry/react";
// 一些工具函数导入
import getArxivPapers from "./GetArxiv";
import getSemanticPapers from "./GetSemantic";
import { fetchPubMedData } from "./GetPubMed ";
import { getTopicFromAI, sendMessageToOpenAI } from "./chatAI";
import { sendMessageToOpenAI } from "./chatAI";
import {
getTextBeforeCursor,
convertToSuperscript,
removeSpecialCharacters,
formatTextInEditor,
getNumberBeforeCursor,
formatJournalReference,
} from "@/utils/others/quillutils";
import { evaluateTopicMatch } from "@/utils/others/aiutils";
//组件
import ExportDocx from "./Export";
import ReferenceList from "./ReferenceList";
import ProgressDisplay from "./ProgressBar";
//redux
import { useAppDispatch, useAppSelector } from "@/app/store";
import {
addReferencesRedux,
setEditorContent,
setApiKey,
setUpsreamUrl,
} from "@/app/store/slices/authSlice";
import { setContentUpdatedFromNetwork } from "@/app/store/slices/stateSlice";
//类型声明
import { Reference } from "@/utils/global";
//supabase
import { createClient } from "@/utils/supabase/client";
import {
getUserPapers,
getUser,
submitPaper,
} from "@/utils/supabase/supabaseutils";
//debounce
import { debounce } from "lodash";
//i18n
import { useTranslation } from "@/app/i18n/client";
//notification
import { ToastContainer, toast } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import { showExpandableToast } from "@/components/Notification";
const toolbarOptions = [
["bold", "italic", "underline", "strike"], // 加粗、斜体、下划线和删除线
@ -47,62 +71,85 @@ const toolbarOptions = [
["clean"], // 清除格式按钮
];
const QEditor = () => {
const QEditor = ({ lng }) => {
//i18n
const { t } = useTranslation(lng);
//读取redux中的API key
const apiKey = useAppSelector((state: any) => state.auth.apiKey);
const upsreamUrl = useAppSelector((state: any) => state.auth.upsreamUrl);
const [quill, setQuill] = useState(null);
//询问ai用户输入
const [userInput, setUserInput] = useState("robot");
//quill编辑器鼠标位置
const [cursorPosition, setCursorPosition] = useState(null);
const isJumpToReference = useAppSelector(
(state) => state.state.isJumpToReference
);
const isEvaluateTopicMatch = useAppSelector(
(state) => state.state.isEvaluateTopicMatch
);
const [quill, setQuill] = useState<Quill | null>(null);
const contentUpdatedFromNetwork = useAppSelector(
(state) => state.state.contentUpdatedFromNetwork
);
//vip状态
const isVip = useAppSelector((state) => state.state.isVip);
//询问ai用户输入
const [userInput, setUserInput] = useState("");
//quill编辑器鼠标位置
const [cursorPosition, setCursorPosition] = useLocalStorage<number | null>(
"光标位置",
0
);
//
// 初始化 Quill 编辑器
const isMounted = useRef(false);
const editor = useRef(null);
const editor = useRef<Quill | null>(null);
// 选择论文来源
const [selectedSource, setSelectedSource] = useLocalStorage(
"semanticScholar",
"semanticScholar"
"学术引擎",
"pubmed"
); // 默认选项
//选择语言模型
const [selectedModel, setSelectedModel] = useLocalStorage("gpt3.5", "gpt3.5"); // 默认选项
//更新参考文献的部分
// const [references, setReferences] = useLocalStorage<Reference[]>(
// "referencesKey",
// undefined
// );
//redux
const [selectedModel, setSelectedModel] = useLocalStorage(
"gpt语言模型",
"deepseek-chat"
); // 默认选项
const [generatedPaperNumber, setGeneratedPaperNumber] = useLocalStorage(
"生成次数",
1
); // 初始值设为1
//选择时间范围
const [timeRange, setTimeRange] = useLocalStorage("时间范围", "2019");
const [generateNumber, setGenerateNumber] = useState(0); //当前任务的进行数
const [openProgressBar, setOpenProgressBar] = useState(false); //设置进度条是否打开
const [showAnnouncement, setShowAnnouncement] = useLocalStorage(
"显示公告",
false
); // 是否显示公告
const [controller, setController] = useState<AbortController | null>(null); // 创建 AbortController 的状态
//redux
const dispatch = useAppDispatch();
const references = useAppSelector((state) => state.auth.referencesRedux);
const editorContent = useAppSelector((state) => state.auth.editorContent); // 从 Redux store 中获取编辑器内容
const addReference = (newReference: Reference) => {
setReferences((prevReferences) => [...prevReferences, newReference]);
};
const removeReference = (index: number) => {
setReferences((prevReferences) =>
prevReferences.filter((_, i) => i !== index)
);
};
const systemPrompt = useAppSelector((state) => state.auth.systemPrompt);
const paperNumberRedux = useAppSelector(
(state) => state.state.paperNumberRedux
);
//supabase
const supabase = createClient();
useEffect(() => {
if (!isMounted.current) {
editor.current = new Quill("#editor", {
modules: {
toolbar: toolbarOptions,
history: {
delay: 2000,
maxStack: 500, // 调整撤销和重做堆栈的大小
userOnly: false,
},
},
theme: "snow",
});
// 检查 localStorage 中是否有保存的内容
// const savedContent = localStorage.getItem("quillContent");
// if (savedContent) {
// // 设置编辑器的内容
// editor.current.root.innerHTML = savedContent;
// }
// 设置编辑器的内容
if (editorContent) {
editor.current.root.innerHTML = editorContent;
}
@ -119,191 +166,415 @@ const QEditor = () => {
console.log("No selection or cursor in the editor.");
}
});
// 添加点击事件监听器
const handleEditorClick = (e) => {
if (isJumpToReference) {
const range = editor.current!.getSelection();
if (range && range.length === 0 && editor.current) {
const [leaf, offset] = editor.current.getLeaf(range.index);
if (leaf.text) {
const textWithoutSpaces = leaf.text.replace(/\s+/g, ""); // 去掉所有空格
if (/^\[\d+\]$/.test(textWithoutSpaces)) {
console.log("点击了引用", textWithoutSpaces);
try {
document.getElementById(textWithoutSpaces)!.scrollIntoView();
} catch (e) {
console.log("没有找到对应的引用");
}
}
}
} else {
console.log("No editor in click.");
}
}
};
editor.current.root.addEventListener("click", handleEditorClick);
// 清理函数
// return () => {
// editor.current!.root.removeEventListener("click", handleEditorClick);
// };
}
}, []);
// 监听editorContent变化(redux的变量)并使用Quill API更新内容
useEffect(() => {
if (editor.current) {
if (editorContent) {
if (contentUpdatedFromNetwork) {
// 清空当前内容
editor.current.setContents([]);
// 插入新内容
editor.current.clipboard.dangerouslyPasteHTML(editorContent);
// 重置标志
dispatch(setContentUpdatedFromNetwork(false));
} else {
console.log("No content updated from network in useEffect.");
}
} else {
console.log("No editorContent to update in useEffect.");
}
} else {
console.log("No editor.current to update in useEffect.");
}
}, [editorContent, contentUpdatedFromNetwork]);
//日常通知可以放在这里
useEffect(() => {
if (showAnnouncement) {
toast(
"📢 如果遇到模型无法响应的情况建议右上角切换为coze模型也是gpt4",
{
position: "top-center",
autoClose: false, // 设置为 false使得公告需要用户手动关闭确保用户看到公告信息
closeOnClick: false, // 防止用户意外点击关闭公告
pauseOnHover: true, // 鼠标悬停时暂停自动关闭,因为 autoClose 已设为 false此设置可保留或去除
draggable: true, // 允许用户拖动公告
progress: undefined,
closeButton: true, // 显示关闭按钮,让用户可以在阅读完毕后关闭公告
hideProgressBar: true, // 隐藏进度条,因为公告不会自动关闭
style: {
// 自定义样式,使公告更加显眼
backgroundColor: "#fffae6", // 浅黄色背景
color: "#333333", // 文字颜色
fontWeight: "bold",
fontSize: "16px",
border: "1px solid #ffd700", // 边框颜色
boxShadow: "0px 0px 10px #ffd700", // 添加阴影,增加显眼度
},
// 当公告被关闭时,设置 localStorage以防再次显示
onClose: () => setShowAnnouncement(false),
}
);
}
}, []);
// 强制更新为我设置的API
// useEffect(() => {
// dispatch(setApiKey("sk-GHuPUV6ERD8wVmmr36FeB8D809D34d93Bb857c009f6aF9Fe"));
// dispatch(setUpsreamUrl("https://one.paperai.life"));
// });
useEffect(() => {
if (upsreamUrl === "https://one.liuweiqing.top") {
dispatch(
setApiKey("sk-GHuPUV6ERD8wVmmr36FeB8D809D34d93Bb857c009f6aF9Fe")
);
dispatch(setUpsreamUrl("https://one.paperai.life"));
}
}, [upsreamUrl]);
const handleTextChange = debounce(async function (delta, oldDelta, source) {
if (source === "user") {
// 获取编辑器内容
const content = quill!.root.innerHTML; // 或 quill.getText(),或 quill.getContents()
dispatch(setEditorContent(content)); // 更新 Redux store
//在云端同步supabase
// console.log("paperNumberRedux in quill", paperNumberRedux);
if (isVip) {
const data = await submitPaper(
supabase,
content,
undefined,
paperNumberRedux
);
}
setTimeout(() => {
convertToSuperscript(quill!);
}, 0); // 延迟 0 毫秒,即将函数放入事件队列的下一个循环中执行,不然就会因为在改变文字触发整个函数时修改文本内容造成无法找到光标位置
}
}, 1000); // 这里的 1000 是防抖延迟时间,单位为毫秒
useEffect(() => {
if (quill) {
// 设置监听器以处理内容变化
quill.on("text-change", function (delta, oldDelta, source) {
if (source === "user") {
// 获取编辑器内容
const content = quill.root.innerHTML; // 或 quill.getText(),或 quill.getContents()
// 保存到 localStorage
// localStorage.setItem("quillContent", content);
dispatch(setEditorContent(content)); // 更新 Redux store
setTimeout(() => {
convertToSuperscript(quill);
}, 0); // 延迟 0 毫秒,即将函数放入事件队列的下一个循环中执行,不然就会因为在改变文字触发整个函数时修改文本内容造成无法找到光标位置
}
});
quill.on("text-change", handleTextChange);
// 清理函数
return () => {
quill.off("text-change", handleTextChange);
};
}
}, [quill, dispatch]);
}, [quill, dispatch, paperNumberRedux]);
// 处理用户输入变化
const handleInputChange = (event) => {
const handleInputChange = (event: any) => {
setUserInput(event.target.value);
};
// 处理AI写作
const handleAIWrite = async () => {
quill.setSelection(cursorPosition, 0); // 将光标移动到原来的位置
const prompt = "请帮助用户完成论文写作,使用用户所说的语言完成";
await sendMessageToOpenAI(
userInput,
quill,
selectedModel,
apiKey,
upsreamUrl,
prompt
);
// 处理输入generatedPaperNumber变化的函数
const handleGeneratedPaperNumberChange = (event: any) => {
const newValue = parseInt(event.target.value, 10);
setGeneratedPaperNumber(newValue);
};
// 处理paper2AI
async function paper2AI(topic: string) {
quill.setSelection(cursorPosition, 0); // 将光标移动到原来的位置
// 处理handleAIAction
async function handleAIAction(topic: string, actionType: string) {
// 创建一个新的 AbortController 实例
const newController = new AbortController();
setController(newController);
quill!.setSelection(cursorPosition!, 0); // 将光标移动到原来的位置
setOpenProgressBar(true); //开启进度条
try {
if (!topic) {
//使用ai提取当前要请求的论文主题
const prompt =
"As a topic extraction assistant, you can help me extract the current discussion of the paper topic, I will enter the content of the paper, you extract the paper topic , no more than two, Hyphenated query terms yield no matches (replace it with space to find matches) return format is: topic1 topic2";
const userMessage = getTextBeforeCursor(quill, 2000);
topic = await getTopicFromAI(userMessage, prompt, apiKey);
console.log("topic in AI before removeSpecialCharacters", topic);
topic = removeSpecialCharacters(topic);
topic = topic.split(" ").slice(0, 2).join(" ");
//如果超过十个字符就截断
if (topic.length > 10) {
topic = topic.slice(0, 10);
if (actionType === "write") {
// 写作逻辑
const prompt = "请帮助用户完成论文写作,使用用户所说的语言完成";
await sendMessageToOpenAI(
userInput,
quill!,
selectedModel!,
apiKey,
upsreamUrl,
prompt,
cursorPosition!,
true,
newController.signal // 传递 AbortSignal
);
} else if (actionType === "paper2AI") {
// paper2AI 逻辑,根据 actionParam 处理特定任务
let offset = -1;
if (generatedPaperNumber != 1) offset = 0; //如果生成的数量不为1则从0开始
//如果说要评估主题是否匹配的话,就要多获取一些文献
let limit = 2;
if (isEvaluateTopicMatch) {
limit = 4;
}
for (let i = 0; i < generatedPaperNumber!; i++) {
if (!topic) {
//使用ai提取当前要请求的论文主题
const prompt =
"As a topic extraction assistant, you can help me extract the current discussion of the paper topic, I will enter the content of the paper, you extract the paper topic , no more than two, Hyphenated query terms yield no matches (replace it with space to find matches) return format is: topic1 topic2";
const userMessage = getTextBeforeCursor(quill!, 2000);
topic = await sendMessageToOpenAI(
userMessage,
null,
selectedModel!,
apiKey,
upsreamUrl,
prompt,
null,
false,
newController.signal // 传递 AbortSignal
);
console.log("topic in AI before removeSpecialCharacters", topic);
topic = removeSpecialCharacters(topic);
topic = topic.split(" ").slice(0, 2).join(" ");
//如果超过十个字符就截断
if (topic.length > 10) {
topic = topic.slice(0, 10);
}
}
console.log(
"topic in AI:",
topic,
"offset in paper2AI:",
offset,
"limit in paper2AI:",
limit
);
let rawData, dataString, newReferences;
if (selectedSource === "arxiv") {
rawData = await getArxivPapers(topic, limit, offset);
//判断返回的文献是否跟用户输入的主题相关
if (isEvaluateTopicMatch) {
const { relevantPapers, nonRelevantPapers } =
await evaluateTopicMatch(
rawData,
apiKey,
upsreamUrl,
selectedModel!,
topic,
newController.signal
);
rawData = relevantPapers;
}
console.log("arxiv rawdata:", rawData);
// 将 rawData 转换为引用数组
newReferences = rawData.map((entry: any) => ({
url: entry.id,
title: entry.title,
year: entry.published,
author: entry.authors?.slice(0, 3).join(", "),
}));
dataString = rawData
.map((entry: any) => {
return `ID: ${entry.id}\nTime: ${entry.published}\nTitle: ${entry.title}\nSummary: ${entry.abstract}\n\n`;
})
.join("");
} else if (selectedSource === "semanticScholar") {
rawData = await getSemanticPapers(
topic,
`${timeRange}-2024`,
offset,
limit
);
//判断返回的文献是否跟用户输入的主题相关
if (isEvaluateTopicMatch) {
const { relevantPapers, nonRelevantPapers } =
await evaluateTopicMatch(
rawData,
apiKey,
upsreamUrl,
selectedModel!,
topic,
newController.signal
);
rawData = relevantPapers;
}
// 将 rawData 转换为引用数组
newReferences = rawData.map((entry: any) => ({
url: entry.url,
title: entry.title,
year: entry.year,
author: entry.authors?.slice(0, 3).join(", "),
venue: entry.venue,
journal: formatJournalReference(entry),
doi: entry.externalIds.DOI,
}));
dataString = rawData
.map((entry: any) => {
return `Time: ${entry.year}\nTitle: ${entry.title}\nSummary: ${entry.abstract}\n\n`;
})
.join("");
} else if (selectedSource === "pubmed") {
rawData = await fetchPubMedData(
topic,
Number(timeRange)!,
offset,
limit
);
if (!rawData) {
throw new Error("未搜索到文献 from PubMed.");
}
//判断返回的文献是否跟用户输入的主题相关
if (isEvaluateTopicMatch) {
const { relevantPapers, nonRelevantPapers } =
await evaluateTopicMatch(
rawData,
apiKey,
upsreamUrl,
selectedModel!,
topic,
newController.signal
);
rawData = relevantPapers;
}
newReferences = rawData.map((entry: any) => ({
id: entry.id, // 文章的 PubMed ID
title: entry.title, // 文章的标题
abstract: entry.abstract, // 文章的摘要
author: entry.authors?.slice(0, 3).join(", "), // 文章的作者列表,假设为字符串数组
year: entry.year, // 文章的发表日期
journal: entry.journal, // 文章的发表杂志
url: entry.url, // 文章的 URL
source: "PubMed", // 指示这些引用来自 PubMed
doi: entry.doi, // 文章的 DOI
}));
// 打印 newReferences
console.log(newReferences);
dataString = rawData
.map((entry: any) => {
return `Time: ${entry.year}\nTitle: ${entry.title}\nSummary: ${entry.abstract}\n\n`;
})
.join("");
}
// 确保搜索到的论文不超过 3000 个字符
const trimmedMessage =
dataString.length > 3000 ? dataString.slice(0, 3000) : dataString;
// 生成AI PROMPT
const content = `之前用户已经完成的内容上下文:${getTextBeforeCursor(
quill!,
800
)},搜索到的论文内容:${trimmedMessage},${topic},`;
showExpandableToast(`搜索论文完成,搜索到的论文:${trimmedMessage}`);
await sendMessageToOpenAI(
content,
quill!,
selectedModel!,
apiKey,
upsreamUrl,
systemPrompt,
cursorPosition!,
true,
newController.signal // 传递 AbortSignal
);
//在对应的位置添加文献
const nearestNumber = getNumberBeforeCursor(quill!);
dispatch(
addReferencesRedux({
references: newReferences,
position: nearestNumber,
})
);
//修改offset使得按照接下来的顺序进行获取文献
offset += 2;
setGenerateNumber(i + 1);
}
setUserInput(""); // 只有在全部成功之后才清空input内容
}
console.log("topic in AI", topic);
let rawData, dataString;
if (selectedSource === "arxiv") {
rawData = await getArxivPapers(topic);
console.log("arxiv rawdata:", rawData);
// 将 rawData 转换为引用数组
const newReferences = rawData.map((entry) => ({
url: entry.id,
title: entry.title,
year: entry.published,
author: entry.authors?.slice(0, 3).join(", "),
}));
dispatch(addReferencesRedux(newReferences));
dataString = rawData
.map((entry) => {
return `ID: ${entry.id}\nTime: ${entry.published}\nTitle: ${entry.title}\nSummary: ${entry.summary}\n\n`;
})
.join("");
} else if (selectedSource === "semanticScholar") {
rawData = await getSemanticPapers(topic, "2015-2023");
// 将 rawData 转换为引用数组
const newReferences = rawData.map((entry) => ({
url: entry.url,
title: entry.title,
year: entry.year,
author: entry.authors?.slice(0, 3).join(", "),
venue: entry.venue,
journalReference: entry.journal
? `${entry.journal.name}[J], ${entry.year}${
entry.journal.volume ? `, ${entry.journal.volume}` : ""
}${entry.journal.pages ? `: ${entry.journal.pages}` : ""}`
: "",
}));
dispatch(addReferencesRedux(newReferences));
dataString = rawData
.map((entry) => {
return `Time: ${entry.year}\nTitle: ${entry.title}\nSummary: ${entry.abstract}\n\n`;
})
.join("");
} else if (selectedSource === "pubmed") {
rawData = await fetchPubMedData(topic, 2020, 2);
const newReferences = rawData.map((entry) => ({
id: entry.id, // 文章的 PubMed ID
title: entry.title, // 文章的标题
abstract: entry.abstract, // 文章的摘要
author: entry.authors.join(", "), // 文章的作者列表,假设为字符串数组
year: entry.year, // 文章的发表日期
venue: entry.journal, // 文章的发表杂志
url: entry.url, // 文章的 URL
source: "PubMed", // 指示这些引用来自 PubMed
}));
// 打印或进一步处理 newReferences
console.log(newReferences);
dispatch(addReferencesRedux(newReferences));
dataString = rawData
.map((entry) => {
return `Time: ${entry.year}\nTitle: ${entry.title}\nSummary: ${entry.abstract}\n\n`;
})
.join("");
}
// 确保搜索到的论文不超过 3000 个字符
const trimmedMessage =
dataString.length > 3000 ? dataString.slice(0, 3000) : dataString;
//slate的方法
// const content = `需要完成的论文主题:${topic}, 搜索到的论文内容:${trimmedMessage},之前已经完成的内容上下文:${extractText(
// editorValue
// )}`;
const content = `之前用户已经完成的内容上下文:${getTextBeforeCursor(
quill,
500
)},搜索到的论文内容:${trimmedMessage},${topic},`;
sendMessageToOpenAI(content, quill, selectedModel, apiKey, upsreamUrl);
toast.success(
`AI ${actionType == "write" ? "写作" : "文献获取总结"}完成`,
{
position: "top-center",
autoClose: 2000,
pauseOnHover: true,
}
);
} catch (error) {
console.error("Error fetching data:", error);
// 在处理错误后,再次抛出这个错误
throw error;
toast.error(`AI写作出现错误(持续无法使用请切换deepseek模型): ${error}`, {
position: "top-center",
autoClose: 3000,
pauseOnHover: true,
});
Sentry.captureMessage(`AI写作出现错误: ${error}`, "error");
} finally {
// 通用的后处理逻辑
const updatedContent = quill!.root.innerHTML;
dispatch(setEditorContent(updatedContent));
if (isVip) {
//在云端同步supabase
const data = await submitPaper(
supabase,
updatedContent,
references,
paperNumberRedux
);
}
setOpenProgressBar(false);
setGenerateNumber(0); //总的已经生成的数量设置为0 以便下次使用
}
}
// 插入论文信息
// const insertPapers = async (topic: string) => {
// const rawData = await getArxivPapers(topic);
// const dataString = rawData
// .map((entry) => {
// return `ID: ${entry.id}\nPublished: ${entry.published}\nTitle: ${entry.title}\nSummary: ${entry.summary}\n\n`;
// })
// .join("");
// quill.insertText(quill.getLength(), dataString);
// };
const handleStop = () => {
if (controller) {
controller.abort(); // 取消请求
setController(null); // 重置 controller 状态
}
};
return (
<div>
<div className="flex flex-col ">
<div id="Qtoolbar" className="space-y-2 flex justify-between">
<input
type="text"
<textarea
value={userInput}
onChange={handleInputChange}
className="flex-grow shadow appearance-none border rounded py-2 px-3 mr-2 text-grey-darker"
placeholder="点击AI Write就是正常的对话交流点击Paper2AI会根据输入的主题词去寻找对应论文" // 这是你的提示
className="textarea-focus-expand flex-grow shadow appearance-none border rounded py-2 px-3 mr-2 text-grey-darker"
placeholder={t(
"点击AI写作就是正常的对话交流点击寻找文献会根据输入的主题词去寻找对应论文"
)}
/>
<button
onClick={handleAIWrite}
onClick={() => handleAIAction(userInput, "write")}
className="bg-gray-300 hover:bg-gray-400 text-black font-bold py-2 px-4 mr-2 rounded"
>
AI Write
{t("AI写作")}
</button>
{/* <button
onClick={() => insertPapers(userInput)}
className="bg-indigo-500 hover:bg-indigo-700 text-black font-bold py-2 px-4 rounded"
>
Insert Papers
</button> */}
<button
onClick={() => paper2AI(userInput)}
onClick={() => handleAIAction(userInput, "paper2AI")}
className="bg-yellow-500 hover:bg-yellow-600 text-white font-bold py-2 px-4 mr-2 rounded"
>
Paper2AI
{t("Paper2AI")}
</button>
{/* 论文网站 */}
<select
title={t("选择论文来源")}
value={selectedSource}
onChange={(e) => setSelectedSource(e.target.value)}
className=" border border-gray-300 bg-white py-2 px-3 rounded leading-tight focus:outline-none focus:bg-white focus:border-gray-500"
@ -311,46 +582,68 @@ const QEditor = () => {
<option value="arxiv">arxiv</option>
<option value="semanticScholar">semantic scholar</option>
<option value="pubmed">pubmed</option>
{/* 其他来源网站 */}
</select>
{/* AI模型 */}
<select
title={t("选择AI模型")}
value={selectedModel}
onChange={(e) => setSelectedModel(e.target.value)}
className=" border border-gray-300 bg-white py-2 px-3 rounded leading-tight focus:outline-none focus:bg-white focus:border-gray-500"
className=" border border-gray-300 bg-white py-2 px-3 rounded leading-tight focus:outline-none focus:bg-white focus:border-gray-500 "
>
<option value="gpt3.5">gpt3.5</option>
<option value="gpt4">gpt4</option>
{/* 其他来源网站 */}
<option value="gpt-3.5-turbo">gpt-3.5-turbo</option>
<option value="gpt-4">gpt-4</option>
<option value="deepseek-chat">deepseek-chat</option>
<option value="commandr">commandr</option>
<option value="gemini-pro">gemini-pro</option>
<option value="mixtral-8x7b-32768">mixtral-8x7b-32768</option>
<option value="llama2-70b-4096">llama2-70b-4096</option>
</select>
{/* 用户输入自己的API key */}
{/* 进行几轮生成 */}
<input
type="number"
title={t("生成轮数")}
value={generatedPaperNumber}
onChange={handleGeneratedPaperNumberChange}
className="border border-gray-300 text-gray-700 text-sm p-1 rounded w-16"
/>
{/* 时间范围 */}
<input
type="number"
title={t("时间范围")}
value={timeRange}
onChange={(e) => setTimeRange(e.target.value)}
className="border border-gray-300 text-gray-700 text-sm p-1 rounded w-16"
/>
<button
onClick={() => formatTextInEditor(quill)} // 假设 updateIndex 是处理更新操作的函数
className="bg-gray-300 hover:bg-gray-400 text-black font-bold py-2 px-4 rounded"
title={t("更新文中的上标,使得数字顺序排列")}
>
{t("更新索引")}
</button>
</div>
<div>
<div
id="editor"
style={{
width: "calc(100vw - 100px)", // 屏幕宽度减去 100px
minHeight: "250px", // 注意驼峰命名法
maxHeight: "500px",
overflowY: "auto", // overflow-y -> overflowY
border: "1px solid #ccc",
padding: "10px",
}}
></div>
<ReferenceList
// references={references}
// addReference={addReference}
// removeReference={removeReference}
// setReferences={setReferences}
editor={quill}
{openProgressBar ? (
<ProgressDisplay
generatedPaperNumber={generatedPaperNumber!}
i={generateNumber}
/>
) : null}
<div>
<div id="editor"></div>
<ReferenceList editor={quill} lng={lng} />
<ExportDocx editor={quill} />
</div>
{/* 停止生成的按钮只有在开始对话之后才会出现 */}
{openProgressBar ? (
<button
onClick={handleStop}
className="fixed bottom-4 left-4 bg-red-500 hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-opacity-50 active:bg-red-700 text-white font-bold py-2 px-4 rounded transition ease-in-out duration-150 shadow-lg hover:shadow-xl"
>
{t("停止生成")}
</button>
) : null}
<ToastContainer />
</div>
);
};

View File

@ -0,0 +1,18 @@
"use client";
import dynamic from "next/dynamic";
import ReduxProvider from "@/app/store/ReduxProvider";
import LoadingIndicator from "@/components/LoadingIndicator"; // 确保路径正确
// import QEditor from "@/components/QuillEditor";
// 动态导入 QuillEditor 组件,禁用 SSR
const QEditor = dynamic(() => import("@/components/QuillEditor"), {
ssr: false,
loading: () => <LoadingIndicator />,
});
export default function QuillWrapper({ lng }) {
return (
<ReduxProvider>
<QEditor lng={lng} />
</ReduxProvider>
);
}

View File

@ -1,132 +0,0 @@
"use client";
import React, { useState, useEffect } from "react";
import ReactQuill from 'react-quill';
import "quill/dist/quill.snow.css";
// 一些工具函数导入
import getArxivPapers from "./GetArxiv";
import sendMessageToOpenAI from "./chatAI";
const QEditor = () => {
const [quill, setQuill] = useState(null);
const [userInput, setUserInput] = useState("");
const [content, setContent] = useState("");
// 处理内容变化
const handleContentChange = (content) => {
setContent(content);
convertToSuperscript();
};
function convertToSuperscript() {
const text = quill.getText();
const regex = /\[\d+\]/g; // 正则表达式匹配 "[数字]" 格式
let match;
while ((match = regex.exec(text)) !== null) {
const startIndex = match.index;
const length = match[0].length;
// 应用上标格式
quill.formatText(startIndex, length, { script: "super" });
// 重置格式(如果需要)
if (startIndex + length < text.length) {
quill.formatText(startIndex + length, 1, "script", false);
}
}
}
// 处理按钮点击事件来插入文本
const handleButtonClick = () => {
if (quill) {
quill.insertText(quill.getLength(), "Hello, World!");
}
};
// 处理用户输入变化
const handleInputChange = (event) => {
setUserInput(event.target.value);
};
const paper2AI = (topic: string) => {
getArxivPapers(topic).then((rawData) => {
// 将每篇文章的信息转换为字符串
const dataString = rawData
.map((entry) => {
return `ID: ${entry.id}\nPublished: ${entry.published}\nTitle: ${entry.title}\nSummary: ${entry.summary}\n\n`;
})
.join("");
// 将处理后的字符串插入到编辑器中
sendMessageToOpenAI(dataString, quill, quill.getText(), topic);
});
};
// 插入论文信息
const insertPapers = async (topic: string) => {
const rawData = await getArxivPapers(topic);
const dataString = rawData
.map((entry) => {
return `ID: ${entry.id}\nPublished: ${entry.published}\nTitle: ${entry.title}\nSummary: ${entry.summary}\n\n`;
})
.join("");
quill.insertText(quill.getLength(), dataString);
};
return (
<div>
<div className="space-y-2">
<button
onClick={handleButtonClick}
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
>
Insert Text
</button>
<input
type="text"
value={userInput}
onChange={handleInputChange}
className="shadow appearance-none border rounded py-2 px-3 text-grey-darker"
/>
{/*<button
onClick={handleAIClick}
className="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded"
>
Insert AI Text
</button>*/}
<button
onClick={() => insertPapers("gnn")}
className="bg-indigo-500 hover:bg-indigo-700 text-white font-bold py-2 px-4 rounded"
>
Insert Papers
</button>
<button
onClick={() => paper2AI("gnn")}
className="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded"
>
Paper2AI
</button>
</div>
<div
id="editor"
style={{
height: "500px",
width: "600px",
minHeight: "150px", // 注意驼峰命名法
maxHeight: "500px",
overflowY: "auto", // overflow-y -> overflowY
border: "1px solid #ccc",
padding: "10px",
}}
></div>
</div>
);
};
export default QEditor;

View File

@ -0,0 +1,548 @@
"use client";
import React, {
useState,
useEffect,
useRef,
useCallback,
useMemo,
} from "react";
import "react-quill/dist/quill.snow.css"; // 导入Quill的snow主题样式
import { useLocalStorage } from "react-use";
import Link from "next/link";
import dynamic from "next/dynamic";
// 一些工具函数导入
import getArxivPapers from "./GetArxiv";
import getSemanticPapers from "./GetSemantic";
import { fetchPubMedData } from "./GetPubMed ";
import { getAI, sendMessageToOpenAI } from "./chatAI";
import {
getTextBeforeCursor,
convertToSuperscript,
removeSpecialCharacters,
formatTextInEditor,
getNumberBeforeCursor,
formatJournalReference,
} from "@/utils/others/quillutils";
import { evaluateTopicMatch } from "@/utils/others/aiutils";
//组件
import ExportDocx from "./Export";
import ReferenceList from "./ReferenceList";
import ProgressDisplay from "./ProgressBar";
//redux
import { useAppDispatch, useAppSelector } from "@/app/store";
import {
addReferencesRedux,
setEditorContent,
} from "@/app/store/slices/authSlice";
import { setContentUpdatedFromNetwork } from "@/app/store/slices/stateSlice";
//类型声明
import { Reference } from "@/utils/global";
//supabase
import { createClient } from "@/utils/supabase/client";
import {
getUserPapers,
getUser,
submitPaper,
} from "@/utils/supabase/supabaseutils";
//debounce
import { debounce } from "lodash";
//i18n
import { useTranslation } from "@/app/i18n/client";
const toolbarOptions = [
["bold", "italic", "underline", "strike"], // 加粗、斜体、下划线和删除线
["blockquote", "code-block"], // 引用和代码块
[{ header: 1 }, { header: 2 }], // 标题
[{ list: "ordered" }, { list: "bullet" }], // 列表
[{ script: "sub" }, { script: "super" }], // 上标/下标
[{ indent: "-1" }, { indent: "+1" }], // 缩进
[{ direction: "rtl" }], // 文字方向
[{ size: ["small", false, "large", "huge"] }], // 字体大小
[{ header: [1, 2, 3, 4, 5, 6, false] }],
[{ color: [] }, { background: [] }], // 字体颜色和背景色
[{ font: [] }], // 字体
[{ align: [] }], // 对齐方式
["clean"], // 清除格式按钮
];
const QEditor = ({ lng }) => {
//quill初始化
const ReactQuill = useMemo(
() => dynamic(() => import("react-quill"), { ssr: false }),
[]
);
//i18n
const { t } = useTranslation(lng);
//读取redux中的API key
const apiKey = useAppSelector((state: any) => state.auth.apiKey);
const upsreamUrl = useAppSelector((state: any) => state.auth.upsreamUrl);
const isJumpToReference = useAppSelector(
(state) => state.state.isJumpToReference
);
const isEvaluateTopicMatch = useAppSelector(
(state) => state.state.isEvaluateTopicMatch
);
const [quill, setQuill] = useState<any>(null);
const contentUpdatedFromNetwork = useAppSelector(
(state) => state.state.contentUpdatedFromNetwork
);
//vip状态
const isVip = useAppSelector((state) => state.state.isVip);
//询问ai用户输入
const [userInput, setUserInput] = useState("");
//quill编辑器鼠标位置
const [cursorPosition, setCursorPosition] = useLocalStorage<number | null>(
"光标位置",
null
);
//
// 初始化 Quill 编辑器
const isMounted = useRef(false);
const editor = useRef<any>(null);
// 选择论文来源
const [selectedSource, setSelectedSource] = useLocalStorage(
"学术引擎",
"semanticScholar"
); // 默认选项
//选择语言模型
const [selectedModel, setSelectedModel] = useLocalStorage(
"gpt语言模型",
"gpt-4"
); // 默认选项
const [generatedPaperNumber, setGeneratedPaperNumber] = useLocalStorage(
"生成次数",
1
); // 初始值设为1
const [generateNumber, setGenerateNumber] = useState(0); //当前任务的进行数
const [openProgressBar, setOpenProgressBar] = useState(false);
//redux
const dispatch = useAppDispatch();
const references = useAppSelector((state) => state.auth.referencesRedux);
const editorContent = useAppSelector((state) => state.auth.editorContent); // 从 Redux store 中获取编辑器内容
const systemPrompt = useAppSelector((state) => state.auth.systemPrompt);
const paperNumberRedux = useAppSelector(
(state) => state.state.paperNumberRedux
);
//quill配置
const modules = {
toolbar: toolbarOptions,
history: {
delay: 2000,
maxStack: 500, // 调整撤销和重做堆栈的大小
userOnly: false,
},
};
// 监听selection-change事件
const handleSelectionChange = useCallback(
(range: any) => {
if (range) {
// 如果有选区或光标位置变化,则更新光标位置
setCursorPosition(range.index);
} else {
// 没有选区或光标不在编辑器内
console.log("No selection or cursor in the editor.");
}
},
[setCursorPosition]
); // 把setCursorPosition添加到依赖数组中
//supabase
const supabase = createClient();
useEffect(() => {
if (!isMounted.current) {
if (editorContent) {
editor.current.getEditor().root.innerHTML = editorContent;
}
isMounted.current = true;
setQuill(editor.current.getEditor());
// 添加点击事件监听器
const handleEditorClick = (e) => {
if (isJumpToReference) {
const range = quill!.getSelection();
if (range && range.length === 0 && quill) {
const [leaf, offset] = quill.getLeaf(range.index);
if (leaf.text) {
const textWithoutSpaces = leaf.text.replace(/\s+/g, ""); // 去掉所有空格
if (/^\[\d+\]$/.test(textWithoutSpaces)) {
console.log("点击了引用", textWithoutSpaces);
try {
document.getElementById(textWithoutSpaces)!.scrollIntoView();
} catch (e) {
console.log("没有找到对应的引用");
}
}
}
} else {
console.log("No editor in click.");
}
}
};
editor.current
?.getEditor()
.root.addEventListener("click", handleEditorClick);
// 清理函数
// return () => {
// editor.current!.root.removeEventListener("click", handleEditorClick);
// };
}
}, []);
// 监听editorContent变化(redux的变量)并使用Quill API更新内容
useEffect(() => {
if (quill) {
if (editorContent) {
if (contentUpdatedFromNetwork) {
// 清空当前内容
quill.setContents([]);
// 插入新内容
quill.clipboard.dangerouslyPasteHTML(editorContent);
// 重置标志
dispatch(setContentUpdatedFromNetwork(false));
} else {
console.log("No content updated from network in useEffect.");
}
} else {
console.log("No editorContent to update in useEffect.");
}
} else {
console.log("No quill to update in useEffect.");
}
}, [editorContent, contentUpdatedFromNetwork]);
const handleTextChange = debounce(async function (delta, oldDelta, source) {
if (source === "user") {
// 获取编辑器内容
const content = quill!.root.innerHTML; // 或 quill.getText(),或 quill.getContents()
dispatch(setEditorContent(content)); // 更新 Redux store
//在云端同步supabase
// console.log("paperNumberRedux in quill", paperNumberRedux);
if (isVip) {
const data = await submitPaper(
supabase,
content,
undefined,
paperNumberRedux
);
}
setTimeout(() => {
convertToSuperscript(quill!);
}, 0); // 延迟 0 毫秒,即将函数放入事件队列的下一个循环中执行,不然就会因为在改变文字触发整个函数时修改文本内容造成无法找到光标位置
}
}, 1000); // 这里的 1000 是防抖延迟时间,单位为毫秒
useEffect(() => {
if (quill) {
// 设置监听器以处理内容变化
quill.on("text-change", handleTextChange);
// 清理函数
return () => {
quill.off("text-change", handleTextChange);
};
}
}, [quill, dispatch, paperNumberRedux]);
// 处理用户输入变化
const handleInputChange = (event: any) => {
setUserInput(event.target.value);
};
// 处理输入generatedPaperNumber变化的函数
const handleGeneratedPaperNumberChange = (event: any) => {
const newValue = parseInt(event.target.value, 10);
setGeneratedPaperNumber(newValue);
};
// 处理AI写作
const handleAIWrite = async () => {
quill!.setSelection(cursorPosition!, 0); // 将光标移动到原来的位置
const prompt = "请帮助用户完成论文写作,使用用户所说的语言完成";
await sendMessageToOpenAI(
userInput,
quill!,
selectedModel!,
apiKey,
upsreamUrl,
prompt,
cursorPosition!
);
// 清空input内容
setUserInput("");
// 重新获取更新后的内容并更新 Redux store
const updatedContent = quill!.root.innerHTML;
dispatch(setEditorContent(updatedContent));
};
// 处理paper2AI
async function paper2AI(topic: string) {
quill!.setSelection(cursorPosition!, 0); // 将光标移动到原来的位置
let offset = -1;
if (generatedPaperNumber != 1) offset = 0; //如果生成的数量不为1则从0开始
setOpenProgressBar(true); //开启进度条
//如果说要评估主题是否匹配的话,就要多获取一些文献
let limit = 2;
if (isEvaluateTopicMatch) {
limit = 4;
}
for (let i = 0; i < generatedPaperNumber!; i++) {
try {
if (!topic) {
//使用ai提取当前要请求的论文主题
const prompt =
"As a topic extraction assistant, you can help me extract the current discussion of the paper topic, I will enter the content of the paper, you extract the paper topic , no more than two, Hyphenated query terms yield no matches (replace it with space to find matches) return format is: topic1 topic2";
const userMessage = getTextBeforeCursor(quill!, 2000);
topic = await getAI(
userMessage,
prompt,
apiKey,
upsreamUrl,
selectedModel!
);
console.log("topic in AI before removeSpecialCharacters", topic);
topic = removeSpecialCharacters(topic);
topic = topic.split(" ").slice(0, 2).join(" ");
//如果超过十个字符就截断
if (topic.length > 10) {
topic = topic.slice(0, 10);
}
}
console.log("topic in AI", topic);
let rawData, dataString, newReferences;
if (selectedSource === "arxiv") {
rawData = await getArxivPapers(topic, limit, offset);
//判断返回的文献是否跟用户输入的主题相关
if (isEvaluateTopicMatch) {
const { relevantPapers, nonRelevantPapers } =
await evaluateTopicMatch(
rawData,
apiKey,
upsreamUrl,
selectedModel!,
topic
);
rawData = relevantPapers;
}
console.log("arxiv rawdata:", rawData);
// 将 rawData 转换为引用数组
newReferences = rawData.map((entry: any) => ({
url: entry.id,
title: entry.title,
year: entry.published,
author: entry.authors?.slice(0, 3).join(", "),
}));
dataString = rawData
.map((entry: any) => {
return `ID: ${entry.id}\nTime: ${entry.published}\nTitle: ${entry.title}\nSummary: ${entry.summary}\n\n`;
})
.join("");
} else if (selectedSource === "semanticScholar") {
rawData = await getSemanticPapers(topic, "2015-2023", offset, limit);
//判断返回的文献是否跟用户输入的主题相关
if (isEvaluateTopicMatch) {
const { relevantPapers, nonRelevantPapers } =
await evaluateTopicMatch(
rawData,
apiKey,
upsreamUrl,
selectedModel!,
topic
);
rawData = relevantPapers;
}
// 将 rawData 转换为引用数组
newReferences = rawData.map((entry: any) => ({
url: entry.url,
title: entry.title,
year: entry.year,
author: entry.authors?.slice(0, 3).join(", "),
venue: entry.venue,
journal: formatJournalReference(entry),
}));
dataString = rawData
.map((entry: any) => {
return `Time: ${entry.year}\nTitle: ${entry.title}\nSummary: ${entry.abstract}\n\n`;
})
.join("");
} else if (selectedSource === "pubmed") {
rawData = await fetchPubMedData(topic, 2020, offset, limit);
if (!rawData) {
throw new Error("未搜索到文献 from PubMed.");
}
//判断返回的文献是否跟用户输入的主题相关
if (isEvaluateTopicMatch) {
const { relevantPapers, nonRelevantPapers } =
await evaluateTopicMatch(
rawData,
apiKey,
upsreamUrl,
selectedModel!,
topic
);
rawData = relevantPapers;
}
newReferences = rawData.map((entry: any) => ({
id: entry.id, // 文章的 PubMed ID
title: entry.title, // 文章的标题
abstract: entry.abstract, // 文章的摘要
author: entry.authors?.slice(0, 3).join(", "), // 文章的作者列表,假设为字符串数组
year: entry.year, // 文章的发表日期
journal: entry.journal, // 文章的发表杂志
url: entry.url, // 文章的 URL
source: "PubMed", // 指示这些引用来自 PubMed
}));
// 打印 newReferences
console.log(newReferences);
dataString = rawData
.map((entry: any) => {
return `Time: ${entry.year}\nTitle: ${entry.title}\nSummary: ${entry.abstract}\n\n`;
})
.join("");
}
// 确保搜索到的论文不超过 3000 个字符
const trimmedMessage =
dataString.length > 3000 ? dataString.slice(0, 3000) : dataString;
//slate的方法
// const content = `需要完成的论文主题:${topic}, 搜索到的论文内容:${trimmedMessage},之前已经完成的内容上下文:${extractText(
// editorValue
// )}`;
const content = `之前用户已经完成的内容上下文:${getTextBeforeCursor(
quill!,
900
)},搜索到的论文内容:${trimmedMessage},${topic},`;
await sendMessageToOpenAI(
content,
quill!,
selectedModel!,
apiKey,
upsreamUrl,
systemPrompt,
cursorPosition!
);
setUserInput("");
// 重新获取更新后的内容并更新 Redux store
const updatedContent = quill!.root.innerHTML;
dispatch(setEditorContent(updatedContent));
//在对应的位置添加文献
const nearestNumber = getNumberBeforeCursor(quill!);
dispatch(
addReferencesRedux({
references: newReferences,
position: nearestNumber,
})
);
if (isVip) {
//在云端同步supabase
const data = await submitPaper(
supabase,
updatedContent,
references,
paperNumberRedux
);
}
//修改offset使得按照接下来的顺序进行获取文献
offset += 2;
setGenerateNumber(i + 1);
} catch (error) {
// console.error("Error fetching data:", error);
// 在处理错误后,再次抛出这个错误
throw new Error(`Paper2AI出现错误: ${error}`);
}
}
setOpenProgressBar(false);
setGenerateNumber(0); //总的已经生成的数量
}
return (
<div className="flex flex-col ">
<div id="Qtoolbar" className="space-y-2 flex justify-between">
<textarea
value={userInput}
onChange={handleInputChange}
className="textarea-focus-expand flex-grow shadow appearance-none border rounded py-2 px-3 mr-2 text-grey-darker"
placeholder={t(
"点击AI写作就是正常的对话交流点击寻找文献会根据输入的主题词去寻找对应论文"
)}
/>
<button
onClick={handleAIWrite}
className="bg-gray-300 hover:bg-gray-400 text-black font-bold py-2 px-4 mr-2 rounded"
>
{t("AI写作")}
</button>
<button
onClick={() => paper2AI(userInput)}
className="bg-yellow-500 hover:bg-yellow-600 text-white font-bold py-2 px-4 mr-2 rounded"
>
{t("Paper2AI")}
</button>
{/* 论文网站 */}
<select
value={selectedSource}
onChange={(e) => setSelectedSource(e.target.value)}
className=" border border-gray-300 bg-white py-2 px-3 rounded leading-tight focus:outline-none focus:bg-white focus:border-gray-500"
>
<option value="arxiv">arxiv</option>
<option value="semanticScholar">semantic scholar</option>
<option value="pubmed">pubmed</option>
</select>
{/* AI模型 */}
<select
value={selectedModel}
onChange={(e) => setSelectedModel(e.target.value)}
className=" border border-gray-300 bg-white py-2 px-3 rounded leading-tight focus:outline-none focus:bg-white focus:border-gray-500 "
>
<option value="gpt-3.5-turbo">gpt-3.5-turbo</option>
<option value="gpt-4">gpt-4</option>
<option value="deepseek-chat">deepseek-chat</option>
</select>
<input
type="number"
value={generatedPaperNumber}
onChange={handleGeneratedPaperNumberChange}
className="border border-gray-300 text-gray-700 text-sm p-1 rounded w-16"
/>
<button
onClick={() => formatTextInEditor(quill)} // 假设 updateIndex 是处理更新操作的函数
className="bg-gray-300 hover:bg-gray-400 text-black font-bold py-2 px-4 rounded"
>
{t("更新索引")}
</button>
</div>
{openProgressBar ? (
<ProgressDisplay
generatedPaperNumber={generatedPaperNumber!}
i={generateNumber}
/>
) : null}
<div>
<ReactQuill
id="editor"
ref={editor}
modules={modules}
theme="snow"
onChangeSelection={handleSelectionChange}
/>
<ReferenceList editor={quill} lng={lng} />
<ExportDocx editor={quill} />
</div>
</div>
);
};
export default QEditor;

View File

@ -1,35 +1,52 @@
import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import { useLocalStorage } from "react-use";
import { Reference } from "@/utils/global";
import {
copyToClipboard,
formatReferenceForCopy,
formatAllReferencesForCopy,
getFullReference,
renderCitation,
getAllFullReferences,
delteIndexUpdateBracketNumbersInDeltaKeepSelection,
} from "@/utils/others/quillutils";
//删除文献按钮
import ParagraphDeleteButton from "@/components/ParagraphDeleteInterface";
//redux
import { useAppDispatch, useAppSelector } from "@/app/store";
import {
addReferenceRedux,
removeReferenceRedux,
clearReferencesRedux,
swapReferencesRedux,
setReferencesRedux,
} from "@/app/store/slices/authSlice";
import { setCitationStyle } from "@/app/store/slices/stateSlice";
//supabase
import { submitPaper } from "@/utils/supabase/supabaseutils";
import { createClient } from "@/utils/supabase/client";
//i18n
import { useTranslation } from "@/app/i18n/client";
type ReferenceListProps = {
// references: Reference[];
// addReference: (reference: Reference) => void;
// removeReference: (index: number) => void;
// setReferences: any;
editor: any;
lng: string;
};
//引用转换
import Cite from "citation-js";
function ReferenceList({
// references,
// addReference,
// removeReference,
// setReferences,
editor,
}: ReferenceListProps) {
const citationStyles = [
{ name: "中文", template: "custom-chinese" }, // 假设你有一个自定义的“中文”格式
{ name: "APA", template: "apa" },
{ name: "MLA", template: "mla" },
{ name: "Chicago", template: "chicago" },
{ name: "Harvard", template: "harvard" },
{ name: "Vancouver", template: "vancouver" },
{ name: "IEEE", template: "ieee" },
];
function ReferenceList({ editor, lng }: ReferenceListProps) {
//i18n
const { t } = useTranslation(lng);
//自定义文献
const [newTitle, setNewTitle] = useState("");
const [newAuthor, setNewAuthor] = useState("");
const [newYear, setNewYear] = useState("");
@ -38,39 +55,37 @@ function ReferenceList({
//redux
const dispatch = useAppDispatch();
const references = useAppSelector((state) => state.auth.referencesRedux);
const paperNumberRedux = useAppSelector(
(state) => state.state.paperNumberRedux
);
const isVip = useAppSelector((state) => state.state.isVip);
const citationStyle = useAppSelector((state) => state.state.citationStyle);
//supabase
const supabase = createClient();
function moveReferenceUp(index: number) {
setReferences((prevReferences) => {
if (index === 0) return prevReferences; // 如果是第一个元素,不进行操作
console.log("index", index);
const newReferences = [...prevReferences];
const temp = newReferences[index];
newReferences[index] = newReferences[index - 1];
newReferences[index - 1] = temp;
console.log("moveReferenceUp", newReferences); // 调试输出
return newReferences;
});
if (index <= 0 || index >= references.length) {
console.log("index", index);
return; // Index out of bounds or first element
}
dispatch(swapReferencesRedux({ indexA: index, indexB: index - 1 }));
}
function moveReferenceDown(index: number) {
setReferences((prevReferences) => {
if (index === prevReferences.length - 1) return prevReferences; // 如果是最后一个元素,不进行操作
console.log("index", index);
if (index < 0 || index >= references.length - 1) {
console.log("index", index);
return; // Index out of bounds or last element
}
const newReferences = [...prevReferences];
const temp = newReferences[index];
newReferences[index] = newReferences[index + 1];
newReferences[index + 1] = temp;
console.log("moveReferenceDown", newReferences); // 调试输出
return newReferences;
});
dispatch(swapReferencesRedux({ indexA: index, indexB: index + 1 }));
}
function removeReferenceUpdateIndex(index: number) {
// removeReference(index);
function removeReferenceUpdateIndex(index: number, rmPg = false) {
handleRemoveReference(index);
delteIndexUpdateBracketNumbersInDeltaKeepSelection(editor, index);
delteIndexUpdateBracketNumbersInDeltaKeepSelection(editor, index, rmPg);
}
const handleAddReference = (newReference: Reference) => {
@ -84,11 +99,116 @@ function ReferenceList({
const handleClearReferences = () => {
dispatch(clearReferencesRedux());
};
// 状态标志,用于跟踪组件是否首次渲染
const [isFirstRender, setIsFirstRender] = useState(true);
React.useEffect(() => {
// 当组件首次渲染后,设置 isFirstRender 为 false
setIsFirstRender(false);
}, []); // 这个 useEffect 依赖数组为空,所以只会在组件首次渲染后运行
//监听references如果发生变化就提交到服务器
React.useEffect(() => {
if (!isFirstRender && isVip) {
submitPaper(supabase, undefined, references, paperNumberRedux);
}
}, [references]);
async function generateCitation(doi, style) {
try {
const citation = await Cite.async(doi);
const output = citation.format("bibliography", {
format: "text",
template: style,
lang: "en-US",
});
return output;
} catch (error) {
console.error("Error generating citation:", error);
return ""; // Return an empty string in case of error
}
}
useEffect(() => {
const fetchCitations = async () => {
const updatedReferences = await Promise.all(
references.map(async (ref) => {
// 检查是否已经有当前风格的引用
if (!ref[citationStyle]) {
// 如果没有,则生成新的引用
const citationText = await generateCitation(ref.doi, citationStyle);
return { ...ref, [citationStyle]: citationText }; // 添加新的引用到对象
}
return ref; // 如果已有引用,则不做改变
})
);
dispatch(setReferencesRedux(updatedReferences));
};
fetchCitations();
}, [citationStyle]);
const handleStyleChange = (event) => {
dispatch(setCitationStyle(event.target.value));
};
return (
<div className="container mx-auto p-4">
<div className=" mx-auto p-4">
{/* 引用列表显示区域 */}
<ul>
{references &&
references.map(
(reference, index) =>
reference && (
<li key={index} className="mb-3 p-2 border-b">
{/* 显示序号 */}
<span className="font-bold mr-2">[{index + 1}].</span>
{/* {getFullReference(reference)} */}
{/* 根据当前风格渲染引用 */}
{renderCitation(reference, citationStyle)}
{reference.url && (
<a
href={reference.url}
className="text-blue-500 hover:underline"
target="_blank"
rel="noopener noreferrer"
id={`[${(index + 1).toString()}]`}
>
{" "}
({reference.url})
</a>
)}
<button
className="bg-gray-500 hover:bg-gray-700 text-white font-bold py-1 px-2 ml-2 rounded"
onClick={() => moveReferenceUp(index)}
>
</button>
<button
className="bg-gray-500 hover:bg-gray-700 text-white font-bold py-1 px-2 ml-2 rounded"
onClick={() => moveReferenceDown(index)}
>
</button>
<button
className="bg-gray-500 hover:bg-gray-700 text-white font-bold py-1 px-2 ml-2 rounded"
onClick={() =>
copyToClipboard(renderCitation(reference, citationStyle))
}
>
{t("复制")}
</button>
<ParagraphDeleteButton
index={index}
isRemovePaper={true}
removeReferenceUpdateIndex={removeReferenceUpdateIndex}
></ParagraphDeleteButton>
</li>
)
)}
</ul>
{/* 表单区域 */}
<form
onSubmit={(e) => {
id="referenceForm"
onSubmit={async (e) => {
e.preventDefault();
handleAddReference({
title: newTitle,
@ -103,6 +223,7 @@ function ReferenceList({
setNewYear("");
setNewPublisher("");
setNewUrl("");
// submitPaper(supabase, undefined, references, paperNumberRedux);
}}
className="mb-6"
>
@ -112,35 +233,35 @@ function ReferenceList({
type="text"
value={newTitle}
onChange={(e) => setNewTitle(e.target.value)}
placeholder="Title"
placeholder={t("Title")}
/>
<input
className="border p-2 rounded"
type="text"
value={newAuthor}
onChange={(e) => setNewAuthor(e.target.value)}
placeholder="Author"
placeholder={t("Author")}
/>
<input
className="border p-2 rounded"
type="text"
value={newYear}
onChange={(e) => setNewYear(e.target.value)}
placeholder="Year"
placeholder={t("Year")}
/>
<input
className="border p-2 rounded"
type="text"
value={newPublisher}
onChange={(e) => setNewPublisher(e.target.value)}
placeholder="Publisher"
placeholder={t("Publisher")}
/>
<input
className="border p-2 rounded"
type="text"
value={newUrl}
onChange={(e) => setNewUrl(e.target.value)}
placeholder="URL"
placeholder={t("Url")}
/>
</div>
<div className="container mx-auto p-4">
@ -148,18 +269,19 @@ function ReferenceList({
<button
className="bg-gray-300 hover:bg-gray-400 text-black font-bold py-2 px-4 rounded "
type="submit"
form="referenceForm"
>
{t("添加自定义引用")}
</button>
<button
className="bg-gray-300 hover:bg-gray-400 text-black font-bold py-2 px-4 rounded "
type="button"
onClick={() =>
copyToClipboard(formatAllReferencesForCopy(references))
copyToClipboard(getAllFullReferences(references, citationStyle))
}
>
{t("复制所有引用")}
</button>
<button
className="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded "
@ -167,70 +289,34 @@ function ReferenceList({
// onClick={() => setReferences([])} // 设置引用列表为空数组
onClick={() => handleClearReferences()}
>
{t("删除所有引用")}
</button>
{/* 下拉框用于更改引用风格 */}
<div className="mt-4">
<label
htmlFor="citation-style"
className="block text-sm font-medium text-gray-700"
>
:
</label>
<select
id="citation-style"
className="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md"
value={citationStyle}
onChange={handleStyleChange}
>
<option value="apa">APA</option>
<option value="mla">MLA</option>
<option value="chicago">Chicago</option>
<option value="harvard">Harvard</option>
<option value="vancouver">Vancouver</option>
<option value="ieee">IEEE</option>
<option value="custom-chinese"></option>
</select>
</div>
</div>
</div>
</form>
{/* 引用列表显示区域 */}
<ul>
{references &&
references.map(
(reference, index) =>
reference && (
<li key={index} className="mb-3 p-2 border-b">
{/* 显示序号 */}
<span className="font-bold mr-2">[{index + 1}].</span>
{reference.author}. {reference.title}.{" "}
{/* 判断 journal 字段是否存在 */}
{reference.journal ? (
<span>reference.journal. </span>
) : (
<span>
{reference.venue}, {reference.year}.
</span>
)}
{reference.url && (
<a
href={reference.url}
className="text-blue-500 hover:underline"
target="_blank"
rel="noopener noreferrer"
>
{" "}
({reference.url})
</a>
)}
{/* <button
className="bg-gray-500 hover:bg-gray-700 text-white font-bold py-1 px-2 ml-2 rounded"
onClick={() => moveReferenceUp(index)}
>
</button>
<button
className="bg-gray-500 hover:bg-gray-700 text-white font-bold py-1 px-2 ml-2 rounded"
onClick={() => moveReferenceDown(index)}
>
</button> */}
<button
className="bg-gray-500 hover:bg-gray-700 text-white font-bold py-1 px-2 ml-2 rounded"
onClick={() =>
copyToClipboard(formatReferenceForCopy(reference))
}
>
</button>
<button
className="text-red-500 hover:text-red-700 ml-4"
onClick={() => removeReferenceUpdateIndex(index)}
>
X
</button>
</li>
)
)}
</ul>
</div>
);
}

View File

@ -1,14 +1,147 @@
"use client";
// Settings.tsx
import { useAppDispatch, useAppSelector } from "@/app/store";
import { setApiKey, setUpsreamUrl } from "@/app/store/slices/authSlice";
import {
setApiKey,
setUpsreamUrl,
setSystemPrompt,
} from "@/app/store/slices/authSlice";
import {
setIsJumpToReference,
setIsEvaluateTopicMatch,
} from "@/app/store/slices/stateSlice";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faArrowLeft } from "@fortawesome/free-solid-svg-icons";
import Link from "next/link";
import { useLocalStorage } from "react-use";
import { useTranslation } from "@/app/i18n/client";
import { useEffect } from "react";
//公告
// import { ToastContainer, toast } from "react-toastify";
// import "react-toastify/dist/ReactToastify.css";
const Settings = () => {
const Settings = ({ lng }: { lng: string }) => {
//i18n
const { t } = useTranslation(lng);
const CONFIG_OPTIONS = [
// {
// name: t("configurations.蒙恬大将军"),
// apiKey: "sk-jokVJ90l5Swxr5dt2f3b0988C8A442A69f97Ee4eAf7aDcF4",
// upstreamUrl: "https://freeapi.iil.im",
// },
// {
// name: t("configurations.coze"),
// apiKey: "MTIwMjE2ODMyODA1NTk1MTM2MA",
// upstreamUrl: "https://coze.paperai.life",
// },
{
name: t("configurations.deepseek-chat"),
apiKey: "sk-ffe19ebe9fa44d00884330ff1c18cf82",
upstreamUrl: "https://api.deepseek.com",
},
// {
// name: t("configurations.caifree"),
// apiKey: "sk-MaEuOo9qIeWKK3PRCdCb9b3d47E64e36Ad6022724b780592",
// upstreamUrl: "https://one.caifree.com",
// },
// {
// name: t("configurations.官网反代"),
// apiKey: "3b73ec02-3255-4b27-a202-42ab9a6e85ba",
// upstreamUrl: "https://plus.liuweiqing.top",
// },
// {
// name: t("configurations.vv佬"),
// apiKey: "nk-23118",
// upstreamUrl: "https://cocopilot-pool.aivvm.com",
// },
// {
// name: t("configurations.linuxdo"),
// apiKey: "nk-2311676378",
// upstreamUrl: "https://chat.flss.world/api/openai",
// },
{
name: t("configurations.oneapi"),
apiKey: "sk-GHuPUV6ERD8wVmmr36FeB8D809D34d93Bb857c009f6aF9Fe",
upstreamUrl: "https://one.paperai.life",
},
{
name: t("configurations.custom"),
apiKey: "",
upstreamUrl: "",
},
];
//https://freeapi.iil.im sk-GdUOBeCCCpeB16G877C8C62b849c4653A561550bEb79Fe7e
//redux
const dispatch = useAppDispatch();
const apiKey = useAppSelector((state) => state.auth.apiKey);
const upstreamUrl = useAppSelector((state) => state.auth.upsreamUrl);
const systemPrompt = useAppSelector((state) => state.auth.systemPrompt);
const isJumpToReference = useAppSelector(
(state) => state.state.isJumpToReference
);
const isEvaluateTopicMatch = useAppSelector(
(state) => state.state.isEvaluateTopicMatch
);
//state
const [userConfigNumber, setUserConfigNumber] = useLocalStorage(
"userConfigNumber",
"2"
);
const toggleSwitch = (currentState: any, setState: any) => {
setState(!currentState);
};
// useEffect(() => {
// toast("这是一个公告消息!", {
// position: "top-center",
// autoClose: 5000, // 持续时间
// hideProgressBar: false,
// closeOnClick: true,
// pauseOnHover: true,
// draggable: true,
// progress: undefined,
// });
// }, []);
return (
<div className="max-w-md mx-auto p-4">
<div className="max-w-md rounded overflow-hidden shadow-lg bg-blue-gray-100 z-1000 mx-auto ">
<h1 className="font-bold text-3xl">settings</h1>
<br />
<div className="flex justify-end mt-4 mr-4">
<Link href="/" aria-label="Settings">
<FontAwesomeIcon
icon={faArrowLeft}
size="2x"
className="icon-hover"
/>
</Link>
</div>
{/* 配置选择器 */}
<label
className="block text-gray-700 text-sm font-bold mb-2"
htmlFor="config-selector"
>
{t("配置选择器")}
</label>
<select
id="config-selector"
className="mb-4 block appearance-none w-full bg-white border border-gray-400 hover:border-gray-500 px-4 py-2 pr-8 rounded shadow leading-tight focus:outline-none focus:shadow-outline"
onChange={(event) => {
const selectedConfig = CONFIG_OPTIONS[Number(event.target.value)];
dispatch(setApiKey(selectedConfig.apiKey));
dispatch(setUpsreamUrl(selectedConfig.upstreamUrl));
setUserConfigNumber(event.target.value);
console.log("userConfigNumber", userConfigNumber);
}}
value={userConfigNumber}
>
{CONFIG_OPTIONS.map((option, index) => (
<option key={index} value={index}>
{option.name}
</option>
))}
</select>
{/* api key */}
<div className="mb-4">
<label
className="block text-gray-700 text-sm font-bold mb-2"
@ -18,7 +151,8 @@ const Settings = () => {
</label>
<input
id="api-key"
type="text"
type="password"
autoComplete="off"
value={apiKey}
onChange={(event) => dispatch(setApiKey(event.target.value))}
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
@ -29,17 +163,72 @@ const Settings = () => {
className="block text-gray-700 text-sm font-bold mb-2"
htmlFor="upstream-url"
>
Upstream URL:
{t("Upstream URL:")}
</label>
<input
id="upstream-url"
type="text"
value={upstreamUrl} // 这里假设你有一个upstreamUrl状态
value={upstreamUrl}
onChange={(event) => dispatch(setUpsreamUrl(event.target.value))}
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
/>
</div>
{/* systemPrompt */}
<div className="mb-4">
<label
className="block text-gray-700 text-sm font-bold mb-2"
htmlFor="system-prompt"
>
{t("System Prompt(Paper2AI):")}
</label>
<textarea
id="system-prompt"
value={systemPrompt}
onChange={(event) => dispatch(setSystemPrompt(event.target.value))}
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
rows={8}
/>
</div>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
className="sr-only peer"
checked={isJumpToReference}
onChange={() =>
toggleSwitch(isJumpToReference, (value: any) =>
dispatch(setIsJumpToReference(value))
)
}
/>
<div className="w-10 h-4 bg-gray-200 rounded-full peer-checked:bg-blue-600 peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 transition-colors ease-in-out duration-200"></div>
<span
className={`absolute block bg-white w-3 h-3 rounded-full transition ease-in-out duration-200 transform ${
isJumpToReference ? "translate-x-6" : "translate-x-1"
} -translate-y-1/2 top-1/2`}
></span>
{t("鼠标点击段落中的上标跳转到文献引用?")}
</label>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
className="sr-only peer"
checked={isEvaluateTopicMatch}
onChange={() =>
toggleSwitch(isEvaluateTopicMatch, (value: any) =>
dispatch(setIsEvaluateTopicMatch(value))
)
}
/>
<div className="w-10 h-4 bg-gray-200 rounded-full peer-checked:bg-blue-600 peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 transition-colors ease-in-out duration-200"></div>
<span
className={`absolute block bg-white w-3 h-3 rounded-full transition ease-in-out duration-200 transform ${
isJumpToReference ? "translate-x-6" : "translate-x-1"
} -translate-y-1/2 top-1/2`}
></span>
{t("是否检查文献与主题相关性(如果不相关则不会传给AI引用)")}
</label>
{/* <ToastContainer /> */}
</div>
);
};

View File

@ -1,14 +1,42 @@
"use client";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCog } from "@fortawesome/free-solid-svg-icons";
import React, { useState } from "react";
import Link from "next/link";
import SettingsWrapper from "@/components/SettingsWrapper";
const SettingsLink = () => {
const [isVisible, setIsVisible] = useState(false);
// 提取的处理函数
const toggleVisibility = () => {
setIsVisible((prevIsVisible) => !prevIsVisible);
console.log("isVisible", isVisible);
};
return (
// <>
// {/* 使用FontAwesomeIcon图标作为按钮 */}
// <FontAwesomeIcon icon={faCog} size="2x" onClick={toggleVisibility} />
// {/* 根据isVisible状态展示或隐藏组件 */}
// <div
// className={`component-container ${
// isVisible ? "animate-slide-in-right" : ""
// }`}
// >
// <SettingsWrapper />
// </div>
// </>
<Link href="/settings" aria-label="Settings">
<FontAwesomeIcon icon={faCog} size="2x" />
<FontAwesomeIcon icon={faCog} size="2x" className="icon-hover" />
</Link>
);
};
export default SettingsLink;
// <Link href="/settings" aria-label="Settings">
// <FontAwesomeIcon icon={faCog} size="2x" />
// </Link>;

View File

@ -1,12 +1,12 @@
"use client";
import ReduxProvider from "@/app/store/ReduxProvider";
import Settings from "../components/Settings";
import Settings from "@/components/Settings";
export default function SettingsWrapper() {
export default function SettingsWrapper({ lng }) {
return (
<ReduxProvider>
<Settings />
<Settings lng={lng} />
</ReduxProvider>
);
}

View File

@ -0,0 +1,70 @@
// components/SignInWithProvider.tsx
"use client";
import { insertUserProfile } from "@/utils/supabase/supabaseutils";
import { createClient } from "@/utils/supabase/client";
import { useEffect } from "react";
import * as Sentry from "@sentry/react";
import { FaGithub, FaGoogle } from "react-icons/fa";
export function SignInWithProvider({ provider, redirectTo }) {
useEffect(() => {
const supabase = createClient();
const { data } = supabase.auth.onAuthStateChange(async (event, session) => {
if (session && session.provider_token) {
// 用户登录成功,执行后续操作
await insertUserProfile(session.user, supabase);
Sentry.captureMessage(`${provider}登录成功`, "info");
console.log(`${provider}登录成功`);
} else {
Sentry.captureMessage(
`${provider}登录中的其它的event${event}`,
"warning"
);
console.log(`${provider}登录中的其它的event`, event);
}
});
return () => data.subscription.unsubscribe();
}, [provider]);
function getProviderIcon(provider) {
switch (provider) {
case "github":
return <FaGithub />;
case "google":
return <FaGoogle />;
default:
return null;
}
}
const signIn = async () => {
const supabase = createClient();
const { data, error } = await supabase.auth.signInWithOAuth({
provider: provider,
options: {
// redirectTo: redirectTo,
queryParams: {
access_type: "offline",
prompt: "consent",
},
},
});
if (error) {
console.error(`${provider} authentication failed:`, error.message);
}
//profiles表 插入用户信息
await insertUserProfile(data, supabase);
};
return (
<button
onClick={signIn}
className="bg-black text-white rounded-md px-4 py-2 mb-2 flex items-center justify-center gap-2 hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white transition ease-in duration-200 w-full"
>
{getProviderIcon(provider)} Sign In with{" "}
{provider.charAt(0).toUpperCase() + provider.slice(1)}
</button>
);
}

View File

@ -1,9 +1,9 @@
import { Transforms } from "slate";
import { Editor } from "slate";
import { extractText } from "@/utils/others/slateutils";
import Quill from "quill";
import {
updateBracketNumbersInDeltaKeepSelection,
convertToSuperscript,
deleteSameBracketNumber,
} from "@/utils/others/quillutils";
//redux不能在普通函数使用
@ -20,25 +20,22 @@ function isValidApiKey(apiKey: string) {
const sendMessageToOpenAI = async (
content: string,
editor: Editor,
selectedModel: "gpt3.5",
editor: Quill | null,
selectedModel: string,
apiKey: string,
upsreamUrl: string,
prompt?: string
prompt: string,
cursorPosition: number | null,
useEditorFlag = true, // 新增的标志,用于决定操作
signal: AbortSignal
) => {
// console.log("apiKey", apiKey);
// console.log("isValidApiKey(apiKey)", isValidApiKey(apiKey).toString());
// console.log(
// " token的值",
// "Bearer " +
// (isValidApiKey(apiKey) ? apiKey : process.env.NEXT_PUBLIC_OPENAI_API_KEY)
// );
//识别应该使用的模型
let model = selectedModel === "gpt3.5" ? "gpt-3.5-turbo" : "gpt-4";
console.log("upsreamUrl", upsreamUrl);
let model = selectedModel;
console.log("upstreamUrl", upsreamUrl);
// 设置API请求参数
const requestOptions = {
method: "POST",
signal: signal,
headers: {
"Content-Type": "application/json",
// "Upstream-Url": upsreamUrl,
@ -50,7 +47,7 @@ const sendMessageToOpenAI = async (
},
body: JSON.stringify({
model: model,
stream: true,
stream: useEditorFlag, // 根据标志确定是否使用streaming
messages: [
{
role: "system",
@ -61,7 +58,7 @@ const sendMessageToOpenAI = async (
2.使 [1]***[1]*
3.
4.
5.使,
5.使
6.
...[1],...[2]`,
@ -77,74 +74,59 @@ const sendMessageToOpenAI = async (
// 发送API请求
let response;
let responseClone = null; // 用于保存响应内容的变量
try {
response = await fetch(
(upsreamUrl || process.env.NEXT_PUBLIC_AI_URL) + "/v1/chat/completions",
requestOptions
);
if (!response.ok) {
throw new Error("Server responded with an error");
// 检查响应状态码是否为429
if (response.status === 429) {
// 可以在这里处理429错误例如通过UI通知用户
throw new Error("请求过于频繁,请稍后再试。");
} else if (!response.ok) {
// 处理其他类型的HTTP错误
throw new Error(`HTTP错误状态码${response.status}`);
}
// 克隆响应以备后用
responseClone = response.clone();
if (useEditorFlag && editor && cursorPosition !== null) {
const reader = response.body!.getReader();
const decoder = new TextDecoder();
//开始前先进行换行
// editor.focus();
editor.insertText(editor.getSelection(true).index, "\n");
await processResult(reader, decoder, editor);
//搜索是否有相同的括号编号,如果有相同的则删除到只剩一个
convertToSuperscript(editor);
deleteSameBracketNumber(editor, cursorPosition);
updateBracketNumbersInDeltaKeepSelection(editor);
} else {
// 直接返回结果的逻辑
const data = await response.json();
const content = data.choices[0].message.content;
return content; // 或根据需要处理并返回数据
}
} catch (error: any) {
if (error.name === "AbortError") {
console.log("Fetch operation was aborted");
//这里不用产生报错因为是手动停止
return;
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
await processResult(reader, decoder, editor);
convertToSuperscript(editor);
updateBracketNumbersInDeltaKeepSelection(editor);
} catch (error) {
console.error("Error:", error);
// 如果有响应,返回响应的原始内容
if (response) {
const rawResponse = await response.text();
throw new Error(`Error: ${error.message}, Response: ${rawResponse}`);
// 根据是否成功读取响应体来抛出错误
if (responseClone) {
const textResponse = await responseClone.text(); // 从克隆的响应中读取数据
throw new Error(
`请求发生错误: ${error.message}, Response: ${textResponse}`
);
} else {
throw new Error(`请求发生错误: ${error.message}`);
}
// 如果没有响应,只抛出错误
throw error;
}
};
const getTopicFromAI = async (
userMessage: string,
prompt: string,
apiKey: string
) => {
// 设置API请求参数
const requestOptions = {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization:
"Bearer " +
(isValidApiKey(apiKey)
? apiKey
: process.env.NEXT_PUBLIC_OPENAI_API_KEY),
},
body: JSON.stringify({
model: "gpt-3.5-turbo",
stream: false,
messages: [
{
role: "system",
content: prompt,
},
{
role: "user",
content: userMessage,
},
],
}),
};
const response = await fetch(process.env.NEXT_PUBLIC_AI_URL, requestOptions);
const data = await response.json();
const topic = data.choices[0].message.content;
return topic; // 获取并返回回复
};
// 给getTopicFromAI函数创建别名
// export const getFromAI = sendMessageToOpenAI;
async function processResult(reader, decoder, editor) {
let buffer = "";
while (true) {
@ -169,24 +151,26 @@ async function processResult(reader, decoder, editor) {
// 如果 jsonStr 以 "data: " 开头,就移除这个前缀
// 移除字符串首尾的空白字符
jsonStr = jsonStr.trim();
jsonStr = jsonStr.substring(6);
// jsonStr = jsonStr.substring(6);
jsonStr = jsonStr.replace("data:", "");
let dataObject = JSON.parse(jsonStr);
console.log("dataObject", dataObject);
// console.log("dataObject", dataObject);
// 处理 dataObject 中的 content
if (dataObject.choices && dataObject.choices.length > 0) {
let content =
dataObject.choices[0].message?.content ||
dataObject.choices[0].delta?.content;
dataObject.choices[0].delta?.content ??
dataObject.choices[0].message?.content;
if (content) {
// 在当前光标位置插入文本
editor.insertText(editor.getSelection().index, content);
console.log("成功插入:", content);
// editor.focus();
editor.insertText(editor.getSelection(true).index, content);
// console.log("成功插入:", content);
}
}
} catch (error) {
console.error("Failed to parse JSON object:", jsonStr);
console.error("Error:", error);
break;
throw new Error(`
there is a error in parse JSON object: ${jsonStr},
error reason: ${error}`);
}
}
} catch (error) {
@ -195,4 +179,4 @@ async function processResult(reader, decoder, editor) {
}
}
export { getTopicFromAI, sendMessageToOpenAI };
export { sendMessageToOpenAI };

8
dictionaries.js Normal file
View File

@ -0,0 +1,8 @@
import "server-only";
const dictionaries = {
en: () => import("./dictionaries/en.json").then((module) => module.default),
nl: () => import("./dictionaries/nl.json").then((module) => module.default),
};
export const getDictionary = async (locale) => dictionaries[locale]();

11
docker-compose.yml Normal file
View File

@ -0,0 +1,11 @@
version: "3.8" # 使用 Docker Compose 文件版本 3.8,根据需要可以更改
services:
paperai:
image: 14790897/paperai:latest
container_name: paperai_app
ports:
- "3000:3000" # 映射宿主机和容器的端口
# environment: # 设置环境变量
# NEXT_PUBLIC_AI_URL: "自定义AI模型地址"
# NEXT_PUBLIC_OPENAI_API_KEY: "自定义API KEY"
restart: unless-stopped # 容器退出时重启策略

View File

@ -1,17 +1,53 @@
import { NextResponse, type NextRequest } from 'next/server'
import { createClient } from '@/utils/supabase/middleware'
import { NextResponse, type NextRequest } from "next/server";
import { createClient } from "@/utils/supabase/middleware";
import { match } from "@formatjs/intl-localematcher";
import Negotiator from "negotiator";
let locales = ["en", "zh-CN"];
function getLocale(request: NextRequest) {
// 从请求中获取`Accept-Language`头
const headers = {
"accept-language": request.headers.get("accept-language") || undefined,
};
// 使用`Negotiator`根据`Accept-Language`头获取优先语言列表
const languages = new Negotiator({ headers }).languages();
// 定义默认语言
let defaultLocale = "en";
// 使用`match`函数匹配最合适的语言
return match(languages, locales, defaultLocale);
}
export async function middleware(request: NextRequest) {
// Check if there is any supported locale in the pathname
const { pathname } = request.nextUrl;
const pathnameHasLocale = locales.some(
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
);
try {
// This `try/catch` block is only here for the interactive tutorial.
// Feel free to remove once you have Supabase connected.
const { supabase, response } = createClient(request)
const { supabase, response } = createClient(request);
// 如果URL中已经包含地区代码则刷新会话
// Refresh session if expired - required for Server Components
// https://supabase.com/docs/guides/auth/auth-helpers/nextjs#managing-session-with-middleware
await supabase.auth.getSession()
return response
if (pathnameHasLocale) {
await supabase.auth.getSession();
return response;
}
// 如果没有地区代码则重定向到包含首选地区的URL
if (!pathnameHasLocale) {
const locale = getLocale(request);
request.nextUrl.pathname = `/${locale}${pathname}`;
// e.g. incoming request is /products
// The new URL is now /en-US/products
return NextResponse.redirect(request.nextUrl);
}
} catch (e) {
// If you are here, a Supabase client could not be created!
// This is likely because you have not set up environment variables.
@ -20,7 +56,7 @@ export async function middleware(request: NextRequest) {
request: {
headers: request.headers,
},
})
});
}
}
@ -33,6 +69,6 @@ export const config = {
* - favicon.ico (favicon file)
* Feel free to modify this pattern to include more paths.
*/
'/((?!_next/static|_next/image|favicon.png).*)',
"/((?!_next/static|_next/image|favicon.ico|twitter-image.png|opengraph-image.png|manifest.json|site.webmanifest|favicon-32x32.png|favicon-16x16.png|apple-touch-icon.png|android-chrome-512x512.png|android-chrome-192x192.png|service-worker.js|serviceregister.js|global.css|sitemap.xml|robots.txt|api/oauth/callback).*)",
],
}
};

View File

@ -3,18 +3,53 @@ const nextConfig = {
typescript: {
ignoreBuildErrors: true,
},
// async rewrites() {
// return [
// {
// source: "/api/v1/chat/completions", // 用户访问的路径
// destination: "/api/chat", // 实际上被映射到的路径
// },
// {
// source: "/api/paper", // 另一个用户访问的路径
// destination: "/api/chat", // 同样被映射到 common-route
// },
// ];
// },
};
module.exports = nextConfig;
// 配置 @next/bundle-analyzer
const withBundleAnalyzer = require("@next/bundle-analyzer")({
enabled: process.env.ANALYZE === "true",
});
// 首先使用withBundleAnalyzer包装nextConfig
const enhancedConfig = withBundleAnalyzer(nextConfig);
// Injected content via Sentry wizard below
const { withSentryConfig } = require("@sentry/nextjs");
// 然后使用withSentryConfig包装已增强的配置
module.exports = withSentryConfig(
enhancedConfig,
{
// For all available options, see:
// https://github.com/getsentry/sentry-webpack-plugin#options
// Suppresses source map uploading logs during build
silent: true,
org: "liuweiqing-limited",
project: "paperai",
},
{
// For all available options, see:
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
// Upload a larger set of source maps for prettier stack traces (increases build time)
widenClientFileUpload: true,
// Transpiles SDK to be compatible with IE11 (increases bundle size)
transpileClientSDK: true,
// Routes browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers (increases server load)
tunnelRoute: "/monitoring",
// Hides source maps from generated client bundles
hideSourceMaps: true,
// Automatically tree-shake Sentry logger statements to reduce bundle size
disableLogger: true,
// Enables automatic instrumentation of Vercel Cron Monitors.
// See the following for more information:
// https://docs.sentry.io/product/crons/
// https://vercel.com/docs/cron-jobs
automaticVercelMonitors: true,
}
);

3036
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -6,45 +6,64 @@
"start": "next start"
},
"dependencies": {
"@formatjs/intl-localematcher": "^0.5.4",
"@fortawesome/free-solid-svg-icons": "^6.5.1",
"@fortawesome/react-fontawesome": "^0.2.0",
"@juggle/resize-observer": "^3.4.0",
"@lemonsqueezy/lemonsqueezy.js": "^2.0.0",
"@next/third-parties": "^14.1.0",
"@reduxjs/toolkit": "^2.0.1",
"@sentry/nextjs": "^7.101.1",
"@supabase/ssr": "latest",
"@supabase/supabase-js": "latest",
"@types/react-toastify": "^4.1.0",
"add": "^2.0.6",
"autoprefixer": "10.4.15",
"axios": "^1.6.5",
"citation-js": "^0.7.8",
"file-saver": "^2.0.5",
"geist": "^1.0.0",
"i": "^0.3.7",
"i18next": "^23.8.2",
"i18next-browser-languagedetector": "^7.2.0",
"i18next-resources-to-backend": "^1.2.0",
"lodash": "^4.17.21",
"negotiator": "^0.6.3",
"next": "latest",
"next-redux-wrapper": "^8.1.0",
"openai": "^4.24.3",
"postcss": "8.4.31",
"postcss": "8.4.35",
"punycode": "^2.3.1",
"quill": "^1.3.7",
"quill-to-word": "^1.3.0",
"raw-body": "^2.5.2",
"react": "^18.2.0",
"react-cookie": "^7.0.2",
"react-dom": "^18.2.0",
"react-quill": "^2.0.0",
"react-i18next": "^14.0.5",
"react-icons": "^5.0.1",
"react-redux": "^9.1.0",
"react-toastify": "^10.0.4",
"react-transition-group": "^4.4.5",
"react-use": "^17.4.3",
"redux": "^5.0.1",
"redux-logger": "^3.0.6",
"redux-persist": "^6.0.0",
"slate": "^0.101.5",
"slate-history": "^0.100.0",
"slate-hyperscript": "^0.100.0",
"slate-react": "^0.101.5",
"sweetalert2": "^11.10.6",
"tailwindcss": "3.3.3",
"typescript": "5.1.3",
"xml2js": "^0.6.2"
},
"devDependencies": {
"@next/bundle-analyzer": "^14.1.0",
"@types/file-saver": "^2.0.7",
"@types/negotiator": "^0.6.3",
"@types/node": "^20.3.1",
"@types/react": "^18.2.48",
"@types/react-dom": "18.2.5",
"@types/react-transition-group": "^4.4.10",
"@types/redux-logger": "^3.0.12",
"encoding": "^0.1.13"
},
"version": "1.1.0"
"version": "1.9.0"
}

17
pages/_error.jsx Normal file
View File

@ -0,0 +1,17 @@
import * as Sentry from "@sentry/nextjs";
import Error from "next/error";
const CustomErrorComponent = (props) => {
return <Error statusCode={props.statusCode} />;
};
CustomErrorComponent.getInitialProps = async (contextData) => {
// In case this is running in a serverless function, await this in order to give Sentry
// time to send the error before the lambda exits
await Sentry.captureUnderscoreErrorException(contextData);
// This will contain the status code of the response
return Error.getInitialProps(contextData);
};
export default CustomErrorComponent;

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

BIN
public/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
public/favicon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 766 B

BIN
public/favicon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

52
public/manifest.json Normal file
View File

@ -0,0 +1,52 @@
{
"short_name": "Paper AI",
"name": "paper ai 使用真实文献让AI完成论文",
"start_url": "/",
"display": "standalone",
"theme_color": "#ffffff",
"background_color": "#ffffff",
"description": "写论文最高效的方式",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/apple-touch-icon.png",
"sizes": "180x180",
"type": "image/png",
"purpose": "any"
},
{
"src": "/favicon-16x16.png",
"sizes": "16x16",
"type": "image/png",
"purpose": "any"
},
{
"src": "/favicon-32x32.png",
"sizes": "32x32",
"type": "image/png",
"purpose": "any"
},
{
"src": "/favicon.ico",
"sizes": "48x48",
"type": "image/x-icon",
"purpose": "any"
}
],
"dir": "ltr",
"scope": "/",
"orientation": "any",
"related_applications": [],
"prefer_related_applications": false
}

58
public/service-worker.js Normal file
View File

@ -0,0 +1,58 @@
const cacheName = "v1";
const preCacheResources = [
// 添加需要预缓存的资源列表
"/.next/static",
"/public",
"/",
"/index.html",
"/styles/main.css",
"/scripts/main.js",
// 更多资源...
];
// 安装事件:预缓存关键资源
self.addEventListener("install", (e) => {
console.log("Service Worker: Installed");
e.waitUntil(
caches
.open(cacheName)
.then((cache) => {
console.log("Service Worker: Caching Files");
cache.addAll(preCacheResources);
})
.then(() => self.skipWaiting())
);
});
// 激活事件:清理旧缓存
self.addEventListener("activate", (e) => {
console.log("Service Worker: Activated");
e.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cache) => {
if (cache !== cacheName) {
console.log("Service Worker: Clearing Old Cache");
return caches.delete(cache);
}
})
);
})
);
});
// 尝试从网络获取资源,并将响应克隆到缓存
const cacheClone = async (e) => {
const res = await fetch(e.request);
const resClone = res.clone();
const cache = await caches.open(cacheName);
await cache.put(e.request, resClone);
return res;
};
// Fetch 事件:网络优先,然后缓存
self.addEventListener("fetch", (e) => {
e.respondWith(
cacheClone(e)
.catch(() => caches.match(e.request))
.then((res) => res || fetch(e.request))
);
});

17
public/serviceregister.js Normal file
View File

@ -0,0 +1,17 @@
if ("serviceWorker" in navigator) {
window.addEventListener("load", () => {
navigator.serviceWorker.register("/service-worker.js").then(
(registration) => {
// 注册成功
console.log(
"ServiceWorker registration successful with scope: ",
registration.scope
);
},
(err) => {
// 注册失败
console.log("ServiceWorker registration failed: ", err);
}
);
});
}

1
public/site.webmanifest Normal file
View File

@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}

37
sentry.client.config.ts Normal file
View File

@ -0,0 +1,37 @@
// This file configures the initialization of Sentry on the client.
// The config you add here will be used whenever a users loads a page in their browser.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from "@sentry/nextjs";
if (process.env.NODE_ENV === "production") {
Sentry.init({
dsn: "https://523c4056ba48d012c62a377dfc49f647@o4506728662564864.ingest.sentry.io/4506728672264192",
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1,
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
replaysOnErrorSampleRate: 1.0,
// This sets the sample rate to be 10%. You may want this to be 100% while
// in development and sample at a lower rate in production
replaysSessionSampleRate: 0.1,
beforeSend(event, hint) {
// 检查事件是否为通过 `captureMessage` 发送的
if (event.logger === "javascript" && event.message) {
return event; // 允许发送消息事件
}
return null; // 过滤掉其他类型的事件
},
// You can remove this option if you're not planning to use the Sentry Session Replay feature:
integrations: [
Sentry.replayIntegration({
// Additional Replay configuration goes in here, for example:
maskAllText: true,
blockAllMedia: true,
}),
],
});
}

17
sentry.edge.config.ts Normal file
View File

@ -0,0 +1,17 @@
// This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on).
// The config you add here will be used whenever one of the edge features is loaded.
// Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from "@sentry/nextjs";
if (process.env.NODE_ENV === "production") {
Sentry.init({
dsn: "https://523c4056ba48d012c62a377dfc49f647@o4506728662564864.ingest.sentry.io/4506728672264192",
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1,
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
});
}

16
sentry.server.config.ts Normal file
View File

@ -0,0 +1,16 @@
// This file configures the initialization of Sentry on the server.
// The config you add here will be used whenever the server handles a request.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from "@sentry/nextjs";
if (process.env.NODE_ENV === "production") {
Sentry.init({
dsn: "https://523c4056ba48d012c62a377dfc49f647@o4506728662564864.ingest.sentry.io/4506728672264192",
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1,
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
});
}

View File

@ -0,0 +1,56 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Confirm Your Signup</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f4f4f4;
}
.container {
background-color: #fff;
max-width: 600px;
margin: 0 auto;
padding: 20px;
text-align: center;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
h1 {
color: #333;
}
p {
color: #666;
}
.button {
display: inline-block;
margin-top: 20px;
padding: 10px 20px;
background-color: #28a745;
color: #ffffff;
text-decoration: none;
border-radius: 5px;
}
</style>
</head>
<body>
<div class="container">
<h1>Welcome to PaperAI!</h1>
<p>
You're almost ready to start exploring all the powerful features of
PaperAI, including AI writing, literature search, and more.
</p>
<p>
Please click the button below to confirm your signup and activate your
account:
</p>
<a href="{{ .ConfirmationURL }}" class="button">Confirm Signup</a>
<p>
If you didn't sign up for a PaperAI account, please ignore this email.
</p>
</div>
</body>
</html>

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