Compare commits
133 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
03c2ee2dd5 | ||
|
f3faf31925 | ||
|
c90d8ea7ac | ||
|
9a19a8924d | ||
|
a8c5392616 | ||
|
8ad486d2f7 | ||
|
cd4181c897 | ||
|
3868ad4acf | ||
|
ef7c85448e | ||
|
a3a5274ca5 | ||
|
f788b2222f | ||
|
1be61f7a9a | ||
|
f2f9448f28 | ||
|
dba8ec332a | ||
|
b3c8dc1d4a | ||
|
8b8702e04d | ||
|
fe43130b4f | ||
|
a5ec62320d | ||
|
9b31268cda | ||
|
29e2579b1d | ||
|
9ff111edaf | ||
|
01b26d9115 | ||
|
4ce386e372 | ||
|
9022969ac6 | ||
|
d227c78b5b | ||
|
d8cdb70333 | ||
|
9e2510d748 | ||
|
0a232f495a | ||
|
5ccdc5270a | ||
|
6bf8061f0f | ||
|
5d61d49f05 | ||
|
18356b67d9 | ||
|
0bfaacf6ee | ||
|
6243aa5401 | ||
|
64cab48ae3 | ||
|
0090ffd3bb | ||
|
8730415352 | ||
|
2f60e65f91 | ||
|
c8f3a94520 | ||
|
fdcadbec60 | ||
|
e0426cf5fc | ||
|
b5bd878cda | ||
|
7f5af058cd | ||
|
939f5c28e9 | ||
|
f63dd8865b | ||
|
5f3252da6e | ||
|
d808aa3195 | ||
|
84239677f5 | ||
|
04a3e64e64 | ||
|
2267c98d7a | ||
|
c91006564b | ||
|
7243d8bc84 | ||
|
3446ce4ced | ||
|
3fd86d788b | ||
|
dfca412ca6 | ||
|
dfab6991e0 | ||
|
15c3a1f0ac | ||
|
a0ce164b15 | ||
|
6315d48d89 | ||
|
f58ce4c7c4 | ||
|
a0e88d8c8d | ||
|
559b4010c2 | ||
|
17ce170ab3 | ||
|
c037ac1db5 | ||
|
4b64827c1d | ||
|
f8e4cfd205 | ||
|
836aa49847 | ||
|
151a1aa286 | ||
|
d2263c503f | ||
|
a4b368c5c6 | ||
|
c23f83a439 | ||
|
35dbdcc2b2 | ||
|
1cf4c37295 | ||
|
94f960363c | ||
|
186a6750c2 | ||
|
103523ee52 | ||
|
79cc718951 | ||
|
624715afd5 | ||
|
d5fb68ecbe | ||
|
a31dc819a9 | ||
|
efabb39dfc | ||
|
4c78494e4d | ||
|
37fa37e9ac | ||
|
b8f613525a | ||
|
e7aa998ca7 | ||
|
43f222a83e | ||
|
a72329d4a2 | ||
|
1eb3c596f3 | ||
|
65db119a9a | ||
|
478d228313 | ||
|
f8335cb03b | ||
|
b0b8707f05 | ||
|
8a25248c29 | ||
|
a3a499a438 | ||
|
72300bf6eb | ||
|
e4ba9f09db | ||
|
6e807a703d | ||
|
2a7867963e | ||
|
49f396f961 | ||
|
0ffb6e1324 | ||
|
6c37459fe3 | ||
|
93f8889c57 | ||
|
f5bdd4f13d | ||
|
4dc951c211 | ||
|
491a374255 | ||
|
49a757f6c7 | ||
|
a38b9cee52 | ||
|
9b835bbadd | ||
|
cbc1e7068b | ||
|
1ce20dc031 | ||
|
0926b24530 | ||
|
6270f427c3 | ||
|
6f11a809f8 | ||
|
baaf5a33c1 | ||
|
28d63ea406 | ||
|
8b725144c2 | ||
|
228f1672f4 | ||
|
1d15248c5e | ||
|
4b7f847d72 | ||
|
3004ae2a57 | ||
|
5d80cab0af | ||
|
fcd174a952 | ||
|
ff09eee993 | ||
|
b92e425517 | ||
|
81552993be | ||
|
cef12f31a0 | ||
|
0d327febea | ||
|
bedc8a3ce0 | ||
|
6cda6d176a | ||
|
b55cf4929a | ||
|
f05e6c3577 | ||
|
70784c7738 | ||
|
9b27ba0595 |
|
@ -12,4 +12,7 @@ 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
|
||||
NODE_ENV=development
|
||||
# NEXT_PUBLIC_CLIENT_ID=UrgIEI0n03tveTmaOV0IU8qRY4DttGY4
|
||||
# CLIENT_SECRET=ljShbIlIrfULu4BTUVTT4azeR90PtAif
|
||||
# REDIRECT_URI=http://localhost:3000/api/oauth/callback
|
|
@ -7,3 +7,6 @@ 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
|
40
.github/workflows/sync.yml
vendored
Normal 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
|
1
.gitignore
vendored
|
@ -37,6 +37,7 @@ next-env.d.ts
|
|||
|
||||
pass
|
||||
.env
|
||||
# .env.local
|
||||
|
||||
.vercel
|
||||
post.md
|
||||
|
|
59
CHANGELOG.md
|
@ -1,5 +1,64 @@
|
|||
# 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)
|
||||
|
||||
|
||||
|
|
23
README.md
|
@ -1,24 +1,26 @@
|
|||
[English Documentation](./README_en.md)
|
||||
|
||||
<a href="https://paperai.life">
|
||||
<div style="text-align: center;">
|
||||
<img src="./app/favicon.ico" alt="the fastest way to create a paper with real references" style="display: block; margin-left: auto; margin-right: auto;">
|
||||
</div> <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 style="text-align: center;">
|
||||
<a href='https://docs.paperai.life/' style='font-size: 20px;'><strong>文档网站(教程比较详细,推荐在这里观看)</strong></a>
|
||||
<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>克隆并在本地运行</strong></a> ·
|
||||
<a href="#克隆并在本地运行"><strong>克隆并在本地运行</strong></a>
|
||||
</p>
|
||||
<br/>
|
||||
|
||||
|
@ -44,7 +46,7 @@
|
|||
|
||||
上述操作还会将 repo 克隆到 GitHub。
|
||||
|
||||
如果只想在本地开发,而不想部署到 Vercel,[请按以下步骤操作](#clone-and-run-locally)。
|
||||
如果只想在本地开发,而不想部署到 Vercel,[请按以下步骤操作](#克隆并在本地运行)。
|
||||
|
||||
## 镜像运行
|
||||
|
||||
|
@ -91,7 +93,8 @@ 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)的许可
|
||||
|
|
48
README_en.md
|
@ -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.
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
Before Width: | Height: | Size: 23 KiB |
|
@ -1,19 +0,0 @@
|
|||
"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>
|
||||
);
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
import { GeistSans } from "geist/font/sans";
|
||||
import "./globals.css";
|
||||
import { GoogleAnalytics } from "@next/third-parties/google";
|
||||
|
||||
const defaultUrl = process.env.VERCEL_URL
|
||||
? `https://${process.env.VERCEL_URL}`
|
||||
: "http://localhost:3000";
|
||||
|
||||
export const metadata = {
|
||||
metadataBase: new URL(defaultUrl),
|
||||
title: "paper ai 使用真实文献让AI完成论文",
|
||||
description: "写论文最高效的方式",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en" className={GeistSans.className}>
|
||||
<body className="bg-background text-foreground">
|
||||
<main className="min-h-screen flex flex-col items-center">
|
||||
{children}
|
||||
</main>
|
||||
</body>
|
||||
<GoogleAnalytics gaId="G-05DHTG9XQ5" />
|
||||
</html>
|
||||
);
|
||||
}
|
|
@ -10,8 +10,9 @@ import { useTranslation } from "@/app/i18n";
|
|||
import { FooterBase } from "@/components/Footer/FooterBase";
|
||||
//supabase
|
||||
import { insertUserProfile } from "@/utils/supabase/supabaseutils";
|
||||
// signingithub
|
||||
import { SignInGitHub } from "@/components/SignInGitHub";
|
||||
// SignInWithProvider
|
||||
import { SignInWithProvider } from "@/components/SignInWithProvider";
|
||||
import LinuxdoSignin from "@/components/LinuxdoSignin";
|
||||
export default async function Login({
|
||||
searchParams,
|
||||
params: { lng },
|
||||
|
@ -127,13 +128,17 @@ export default async function Login({
|
|||
required
|
||||
/>
|
||||
<button className="bg-green-700 rounded-md px-4 py-2 text-foreground mb-2">
|
||||
Sign In
|
||||
Sign In(登录)
|
||||
</button>
|
||||
<button
|
||||
formAction={signUp}
|
||||
className="border border-foreground/20 rounded-md px-4 py-2 text-foreground mb-2"
|
||||
>
|
||||
Sign Up
|
||||
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">
|
||||
|
@ -141,7 +146,17 @@ export default async function Login({
|
|||
</p>
|
||||
)}
|
||||
</form>
|
||||
<SignInGitHub />
|
||||
<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">
|
||||
{" "}
|
||||
|
|
|
@ -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>
|
||||
}
|
Before Width: | Height: | Size: 278 KiB |
|
@ -14,18 +14,19 @@ import PaperManagementWrapper from "@/components/PaperManagementWrapper";
|
|||
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 {
|
||||
createClient(cookieStore);
|
||||
supabase = createClient(cookieStore);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
|
@ -33,7 +34,12 @@ export default async function Index({ params: { lng } }: IndexProps) {
|
|||
};
|
||||
|
||||
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">
|
||||
|
@ -42,6 +48,8 @@ export default async function Index({ params: { lng } }: IndexProps) {
|
|||
{/* 用来表示是否显示论文列表页 */}
|
||||
<PaperListButtonWrapper />
|
||||
{isSupabaseConnected && <AuthButton />}
|
||||
{/* 如果用户没有登录会出现谷歌的sign in按钮登录之后不会出现 */}
|
||||
{!user && <GoogleSignIn />}
|
||||
<SettingsLink />
|
||||
</div>
|
||||
</nav>
|
||||
|
@ -66,6 +74,20 @@ export default async function Index({ params: { lng } }: IndexProps) {
|
|||
>
|
||||
<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>
|
||||
|
|
70
app/[lng]/privacy/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
41
app/[lng]/request-reset/page.tsx
Normal 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;
|
58
app/[lng]/reset-password/page.tsx
Normal 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;
|
50
app/[lng]/service/page.tsx
Normal 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;
|
Before Width: | Height: | Size: 278 KiB |
46
app/[lng]/welcome/page.tsx
Normal 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 />
|
||||
</>
|
||||
);
|
||||
}
|
106
app/api/oauth/callback/route.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
BIN
app/favicon.ico
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 15 KiB |
|
@ -123,17 +123,24 @@
|
|||
#editor {
|
||||
width: 100%; /* 适应屏幕宽度 */
|
||||
min-height: 200px; /* 调整为更适合移动设备的尺寸 */
|
||||
padding: 5px; /* 减少内边距 */
|
||||
}
|
||||
|
||||
#Qtoolbar {
|
||||
flex-direction: column; /* 在较小屏幕上垂直堆叠工具栏元素 */
|
||||
align-items: stretch; /* 拉伸按钮以填充容器 */
|
||||
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 > select,
|
||||
#Qtoolbar > input {
|
||||
margin-bottom: 10px; /* 增加元素之间的间距 */
|
||||
margin-right: 10px; /* 右边距,增加元素之间的间距 */
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -160,3 +167,8 @@
|
|||
transform: scale(1.2); /* 放大到原大小的1.2倍 */
|
||||
transition: transform 0.3s ease; /* 平滑过渡效果 */
|
||||
}
|
||||
|
||||
/* 去除toast的最大宽度限制 */
|
||||
.Toastify__toast-container--top-center:has(div.toastDetail) {
|
||||
width: 80%;
|
||||
}
|
|
@ -4,6 +4,12 @@
|
|||
"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",
|
||||
|
@ -24,7 +30,14 @@
|
|||
"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?"
|
||||
"鼠标点击段落中的上标跳转到文献引用?": "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)"
|
||||
}
|
||||
|
|
|
@ -4,6 +4,12 @@
|
|||
"AI写作": "AI写作",
|
||||
"Paper2AI": "寻找文献",
|
||||
"点击AI写作就是正常的对话交流,点击寻找文献会根据输入的主题词去寻找对应论文": "点击AI写作就是正常的对话交流,点击寻找文献会根据输入的主题词去寻找对应论文",
|
||||
"选择论文来源": "选择论文来源",
|
||||
"选择AI模型": "选择AI模型",
|
||||
"生成轮数": "生成轮数",
|
||||
"时间范围": "文献发布日期范围,从这个时间到今年",
|
||||
"更新文中的上标,使得数字顺序排列": "更新文中的上标,使得数字顺序排列",
|
||||
"停止生成": "停止生成",
|
||||
"+ Add Paper": "+ 添加新论文(会直接替换编辑器里的内容)",
|
||||
"Buy VIP TO UNLOCK Cloud Sync and Edit Mutiple Papers Simultaneously": "购买VIP解锁云同步和同时编辑多篇论文",
|
||||
"Paper Management": "论文管理",
|
||||
|
@ -25,7 +31,14 @@
|
|||
"cocopilot-gpt4": "cocopilot-gpt4(apiKey前面手动加上ghu,因为GitHub不允许上传完整的密钥)",
|
||||
"deepseek-chat": "deepseek-chat(需要手动修改模型为这个)",
|
||||
"caifree": "caifree(推荐)",
|
||||
"linuxdo": "linuxdo",
|
||||
"coze": "扣子coze(我亲自维护)",
|
||||
"官网反代": "官网反代",
|
||||
"vv佬": "vv佬(推荐)",
|
||||
"蒙恬大将军": "蒙恬大将军(推荐)",
|
||||
"oneapi": "oneapi",
|
||||
"custom": "自定义"
|
||||
},
|
||||
"鼠标点击段落中的上标跳转到文献引用?": "鼠标点击段落中的上标跳转到文献引用?"
|
||||
"鼠标点击段落中的上标跳转到文献引用?": "鼠标点击段落中的上标跳转到文献引用?",
|
||||
"是否检查文献与主题相关性(如果不相关则不会传给AI引用)": "是否检查文献与主题相关性(如果不相关则不会传给AI引用)"
|
||||
}
|
||||
|
|
109
app/layout.tsx
Normal file
|
@ -0,0 +1,109 @@
|
|||
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({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
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>
|
||||
);
|
||||
}
|
5
app/robots.txt
Normal file
|
@ -0,0 +1,5 @@
|
|||
User-Agent: *
|
||||
Allow: /
|
||||
Disallow: /private/
|
||||
|
||||
Sitemap: https://www.paperai.life/sitemap.xml
|
30
app/sitemap.ts
Normal 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,
|
||||
},
|
||||
];
|
||||
}
|
|
@ -29,6 +29,8 @@ const statePersistConfig = {
|
|||
"isVip",
|
||||
"language",
|
||||
"isJumpToReference",
|
||||
"isEvaluateTopicMatch",
|
||||
"citationStyle",
|
||||
],
|
||||
};
|
||||
|
||||
|
|
|
@ -10,10 +10,10 @@ export interface APIState {
|
|||
}
|
||||
|
||||
const initialState: APIState = {
|
||||
apiKey: "sk-aiHrrRLYUUelHstX69E9484509254dBf92061d6744FfFaD1", //sk-ffe19ebe9fa44d00884330ff1c18cf82
|
||||
apiKey: "sk-GHuPUV6ERD8wVmmr36FeB8D809D34d93Bb857c009f6aF9Fe", //sk-ffe19ebe9fa44d00884330ff1c18cf82
|
||||
referencesRedux: [],
|
||||
editorContent: "",
|
||||
upsreamUrl: "https://one.caifree.com", //https://api.openai.com https://one.caifree.com https://chatserver.3211000.xyz https://api.deepseek.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]*。
|
||||
|
|
|
@ -6,6 +6,8 @@ export interface APIState {
|
|||
isVip: boolean;
|
||||
language: string;
|
||||
isJumpToReference: boolean;
|
||||
isEvaluateTopicMatch: boolean;
|
||||
citationStyle: string;
|
||||
}
|
||||
|
||||
const initialState: APIState = {
|
||||
|
@ -15,6 +17,8 @@ const initialState: APIState = {
|
|||
isVip: false,
|
||||
language: "en",
|
||||
isJumpToReference: false,
|
||||
isEvaluateTopicMatch: false,
|
||||
citationStyle: "custom-chinese",
|
||||
};
|
||||
|
||||
export const stateSlice = createSlice({
|
||||
|
@ -45,6 +49,12 @@ export const stateSlice = createSlice({
|
|||
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;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -56,6 +66,8 @@ export const {
|
|||
setIsVip,
|
||||
setLanguage,
|
||||
setIsJumpToReference,
|
||||
setIsEvaluateTopicMatch,
|
||||
setCitationStyle,
|
||||
} = stateSlice.actions;
|
||||
|
||||
export const stateReducer = stateSlice.reducer;
|
||||
|
|
|
@ -1,19 +1,20 @@
|
|||
// "use server";
|
||||
|
||||
import { createClient } from "@/utils/supabase/server";
|
||||
import Link from "next/link";
|
||||
import { cookies } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
// import { signOut } from "@/utils/supabase/serversignout";
|
||||
import { insertUserProfile } from "@/utils/supabase/supabaseutils";
|
||||
|
||||
export default async function AuthButton() {
|
||||
const cookieStore = cookies();
|
||||
const supabase = createClient(cookieStore);
|
||||
|
||||
const {
|
||||
data,
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser();
|
||||
|
||||
//profiles表 插入用户信息
|
||||
await insertUserProfile(data, supabase);
|
||||
// console.log("1111 in AuthButton user:", user);
|
||||
const signOut = async () => {
|
||||
"use server";
|
||||
|
||||
|
|
|
@ -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"
|
||||
>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
"use client";
|
||||
import { useCallback } from "react";
|
||||
import { saveAs } from "file-saver";
|
||||
import * as quillToWord from "quill-to-word";
|
||||
|
@ -12,6 +13,7 @@ type ParaIn = {
|
|||
|
||||
const ExportDocx = ({ editor }: ParaIn) => {
|
||||
const references = useAppSelector((state) => state.auth.referencesRedux);
|
||||
const citationStyle = useAppSelector((state) => state.state.citationStyle);
|
||||
|
||||
const prepareReferencesForQuill = (references: Reference[]) => {
|
||||
// 首先添加一个标题
|
||||
|
@ -24,8 +26,17 @@ const ExportDocx = ({ editor }: ParaIn) => {
|
|||
insert: "\n参考文献\n",
|
||||
},
|
||||
];
|
||||
const referencesString = getAllFullReferences(references);
|
||||
const quillReferences = [{ insert: referencesString }];
|
||||
const referencesString = getAllFullReferences(references, citationStyle);
|
||||
const quillReferences = [
|
||||
{
|
||||
attributes: {
|
||||
// 提供默认值,即使这些值不会改变文本样式
|
||||
bold: false, // 默认为false,因为引用通常不需要加粗
|
||||
align: "left", // 默认为left,这是大多数文本的常规对齐方式
|
||||
},
|
||||
insert: referencesString,
|
||||
},
|
||||
];
|
||||
// 合并标题和引用列表
|
||||
return referencesWithTitle.concat(quillReferences);
|
||||
};
|
||||
|
|
|
@ -57,7 +57,7 @@ async function getArxivPapers(
|
|||
return result;
|
||||
} catch (error: any) {
|
||||
throw new Error(
|
||||
`Error fetching data from Arxiv API:${JSON.stringify(
|
||||
`Arxiv失败(请使用英文并缩短关键词):${JSON.stringify(
|
||||
error.response,
|
||||
null,
|
||||
2
|
||||
|
@ -76,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]),
|
||||
};
|
||||
});
|
||||
|
|
|
@ -23,7 +23,7 @@ async function getPubMedPapers(
|
|||
const retMax = limit; // 检索的最大记录数
|
||||
const maxOffset = 20 - limit; // 假设总记录数为 20
|
||||
if (offset === -1) offset = getRandomOffset(maxOffset);
|
||||
const url = `${baseURL}?db=${db}&term=${query}[Title/Abstract]+AND+2018:3000[Date - Publication]&retMax=${retMax}&retStart=${offset}&api_key=${process.env.NEXT_PUBLIC_PUBMED_API_KEY}`;
|
||||
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数据
|
||||
|
@ -37,7 +37,7 @@ async function getPubMedPapers(
|
|||
// 这里只返回了ID列表,你可能需要根据实际需要进行调整
|
||||
return idList;
|
||||
} catch (error) {
|
||||
console.error("Error fetching data from PubMed API:", error);
|
||||
console.error(" PubMed API失败(请使用英文并缩短关键词):", error);
|
||||
return null; // 或根据需要处理错误
|
||||
}
|
||||
}
|
||||
|
@ -66,18 +66,18 @@ async function getPubMedPaperDetails(idList: IDList) {
|
|||
// 解析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;
|
||||
|
@ -85,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 直接就是字符串
|
||||
|
@ -133,7 +133,7 @@ async function getPubMedPaperDetails(idList: IDList) {
|
|||
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;
|
||||
|
@ -141,22 +141,43 @@ async function getPubMedPaperDetails(idList: IDList) {
|
|||
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._,
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ async function getSemanticPapers(
|
|||
try {
|
||||
const maxOffset = 20 - limit; // 假设总记录数为 20
|
||||
if (offset === -1) offset = getRandomOffset(maxOffset);
|
||||
const url = `https://api.semanticscholar.org/graph/v1/paper/search`;
|
||||
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,
|
||||
|
@ -32,9 +32,10 @@ async function getSemanticPapers(
|
|||
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",
|
||||
},
|
||||
});
|
||||
// 提取并处理论文数据
|
||||
|
@ -51,7 +52,7 @@ async function getSemanticPapers(
|
|||
} catch (error: any) {
|
||||
// console.error("Error fetching data from Semantic Scholar API:", error);
|
||||
throw new Error(
|
||||
`Error fetching data from Semantic Scholar API:${JSON.stringify(
|
||||
`Semantic Scholar fail(请使用英文并缩短关键词):${JSON.stringify(
|
||||
error.response,
|
||||
null,
|
||||
2
|
||||
|
|
93
components/GoogleSignIn.tsx
Normal 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;
|
31
components/LinuxdoSignin.tsx
Normal 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;
|
13
components/LoadingIndicator.tsx
Normal 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;
|
34
components/Notification.tsx
Normal 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",
|
||||
});
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { useCallback, useState, useEffect } from "react";
|
||||
import { useCallback, useState, useRef, useEffect } from "react";
|
||||
//redux
|
||||
import { useAppDispatch, useAppSelector } from "@/app/store";
|
||||
import {
|
||||
|
@ -11,6 +11,7 @@ import {
|
|||
setPaperNumberRedux,
|
||||
setContentUpdatedFromNetwork,
|
||||
setIsVip,
|
||||
setShowPaperManagement,
|
||||
} from "@/app/store/slices/stateSlice";
|
||||
//supabase
|
||||
import { createClient } from "@/utils/supabase/client";
|
||||
|
@ -134,6 +135,33 @@ const PaperManagement = ({ lng }) => {
|
|||
// 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}
|
||||
|
@ -144,7 +172,10 @@ const PaperManagement = ({ lng }) => {
|
|||
{/* showPaperManagement ? ( */}
|
||||
{/* <animated.div style={animations}> */}
|
||||
<>
|
||||
<div className="paper-management-container flex flex-col items-center space-y-4">
|
||||
<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">
|
||||
{" "}
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
"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,
|
||||
|
@ -19,6 +19,7 @@ import {
|
|||
getNumberBeforeCursor,
|
||||
formatJournalReference,
|
||||
} from "@/utils/others/quillutils";
|
||||
import { evaluateTopicMatch } from "@/utils/others/aiutils";
|
||||
//组件
|
||||
import ExportDocx from "./Export";
|
||||
import ReferenceList from "./ReferenceList";
|
||||
|
@ -28,6 +29,8 @@ import { useAppDispatch, useAppSelector } from "@/app/store";
|
|||
import {
|
||||
addReferencesRedux,
|
||||
setEditorContent,
|
||||
setApiKey,
|
||||
setUpsreamUrl,
|
||||
} from "@/app/store/slices/authSlice";
|
||||
import { setContentUpdatedFromNetwork } from "@/app/store/slices/stateSlice";
|
||||
//类型声明
|
||||
|
@ -43,6 +46,10 @@ import {
|
|||
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"], // 加粗、斜体、下划线和删除线
|
||||
|
@ -74,6 +81,9 @@ const QEditor = ({ lng }) => {
|
|||
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
|
||||
|
@ -86,7 +96,7 @@ const QEditor = ({ lng }) => {
|
|||
//quill编辑器鼠标位置
|
||||
const [cursorPosition, setCursorPosition] = useLocalStorage<number | null>(
|
||||
"光标位置",
|
||||
null
|
||||
0
|
||||
);
|
||||
//
|
||||
// 初始化 Quill 编辑器
|
||||
|
@ -95,19 +105,26 @@ const QEditor = ({ lng }) => {
|
|||
// 选择论文来源
|
||||
const [selectedSource, setSelectedSource] = useLocalStorage(
|
||||
"学术引擎",
|
||||
"semanticScholar"
|
||||
"pubmed"
|
||||
); // 默认选项
|
||||
//选择语言模型
|
||||
const [selectedModel, setSelectedModel] = useLocalStorage(
|
||||
"gpt语言模型",
|
||||
"gpt-4"
|
||||
"deepseek-chat"
|
||||
); // 默认选项
|
||||
const [generatedPaperNumber, setGeneratedPaperNumber] = useLocalStorage(
|
||||
"生成次数",
|
||||
1
|
||||
); // 初始值设为1
|
||||
//选择时间范围
|
||||
const [timeRange, setTimeRange] = useLocalStorage("时间范围", "2019");
|
||||
const [generateNumber, setGenerateNumber] = useState(0); //当前任务的进行数
|
||||
const [openProgressBar, setOpenProgressBar] = useState(false);
|
||||
const [openProgressBar, setOpenProgressBar] = useState(false); //设置进度条是否打开
|
||||
const [showAnnouncement, setShowAnnouncement] = useLocalStorage(
|
||||
"显示公告",
|
||||
false
|
||||
); // 是否显示公告
|
||||
const [controller, setController] = useState<AbortController | null>(null); // 创建 AbortController 的状态
|
||||
|
||||
//redux
|
||||
const dispatch = useAppDispatch();
|
||||
|
@ -202,7 +219,48 @@ const QEditor = ({ lng }) => {
|
|||
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") {
|
||||
// 获取编辑器内容
|
||||
|
@ -244,162 +302,253 @@ const QEditor = ({ lng }) => {
|
|||
const newValue = parseInt(event.target.value, 10);
|
||||
setGeneratedPaperNumber(newValue);
|
||||
};
|
||||
// 处理AI写作
|
||||
const handleAIWrite = async () => {
|
||||
|
||||
// 处理handleAIAction
|
||||
async function handleAIAction(topic: string, actionType: string) {
|
||||
// 创建一个新的 AbortController 实例
|
||||
const newController = new AbortController();
|
||||
setController(newController);
|
||||
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) offset = 0;
|
||||
setOpenProgressBar(true);
|
||||
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 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);
|
||||
}
|
||||
}
|
||||
console.log("topic in AI", topic);
|
||||
let rawData, dataString, newReferences;
|
||||
if (selectedSource === "arxiv") {
|
||||
rawData = await getArxivPapers(topic);
|
||||
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);
|
||||
// 将 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, 2);
|
||||
if (!rawData) {
|
||||
throw new Error("未搜索到文献 from PubMed.");
|
||||
}
|
||||
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},请根据搜索到的论文内容完成用户的论文`;
|
||||
setOpenProgressBar(true); //开启进度条
|
||||
try {
|
||||
if (actionType === "write") {
|
||||
// 写作逻辑
|
||||
const prompt = "请帮助用户完成论文写作,使用用户所说的语言完成";
|
||||
await sendMessageToOpenAI(
|
||||
content,
|
||||
userInput,
|
||||
quill!,
|
||||
selectedModel!,
|
||||
apiKey,
|
||||
upsreamUrl,
|
||||
systemPrompt,
|
||||
cursorPosition!
|
||||
prompt,
|
||||
cursorPosition!,
|
||||
true,
|
||||
newController.signal // 传递 AbortSignal
|
||||
);
|
||||
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
|
||||
);
|
||||
} else if (actionType === "paper2AI") {
|
||||
// paper2AI 逻辑,根据 actionParam 处理特定任务
|
||||
let offset = -1;
|
||||
if (generatedPaperNumber != 1) offset = 0; //如果生成的数量不为1,则从0开始
|
||||
//如果说要评估主题是否匹配的话,就要多获取一些文献
|
||||
let limit = 2;
|
||||
if (isEvaluateTopicMatch) {
|
||||
limit = 4;
|
||||
}
|
||||
//修改offset使得按照接下来的顺序进行获取文献
|
||||
offset += 2;
|
||||
setGenerateNumber(i + 1);
|
||||
} catch (error) {
|
||||
// console.error("Error fetching data:", error);
|
||||
// 在处理错误后,再次抛出这个错误
|
||||
throw new Error(`Paper2AI出现错误: ${error}`);
|
||||
|
||||
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内容
|
||||
}
|
||||
toast.success(
|
||||
`AI ${actionType == "write" ? "写作" : "文献获取总结"}完成`,
|
||||
{
|
||||
position: "top-center",
|
||||
autoClose: 2000,
|
||||
pauseOnHover: true,
|
||||
}
|
||||
);
|
||||
} catch (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 以便下次使用
|
||||
}
|
||||
setOpenProgressBar(false);
|
||||
setGenerateNumber(0); //总的已经生成的数量
|
||||
}
|
||||
|
||||
const handleStop = () => {
|
||||
if (controller) {
|
||||
controller.abort(); // 取消请求
|
||||
setController(null); // 重置 controller 状态
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="flex flex-col ">
|
||||
<div id="Qtoolbar" className="space-y-2 flex justify-between">
|
||||
|
@ -412,19 +561,20 @@ const QEditor = ({ lng }) => {
|
|||
)}
|
||||
/>
|
||||
<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"
|
||||
>
|
||||
{t("AI写作")}
|
||||
</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"
|
||||
>
|
||||
{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"
|
||||
|
@ -435,6 +585,7 @@ const QEditor = ({ lng }) => {
|
|||
</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 "
|
||||
|
@ -442,16 +593,31 @@ const QEditor = ({ lng }) => {
|
|||
<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>
|
||||
{/* 进行几轮生成 */}
|
||||
<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>
|
||||
|
@ -467,6 +633,17 @@ const QEditor = ({ lng }) => {
|
|||
<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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
"use client";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
import ReduxProvider from "@/app/store/ReduxProvider";
|
||||
import QEditor from "@/components/QuillEditor";
|
||||
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>
|
||||
|
|
|
@ -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;
|
548
components/ReactQuillEditorNew.tsx
Normal 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;
|
|
@ -1,9 +1,11 @@
|
|||
import React, { useState } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useLocalStorage } from "react-use";
|
||||
|
||||
import { Reference } from "@/utils/global";
|
||||
import {
|
||||
copyToClipboard,
|
||||
getFullReference,
|
||||
renderCitation,
|
||||
getAllFullReferences,
|
||||
delteIndexUpdateBracketNumbersInDeltaKeepSelection,
|
||||
} from "@/utils/others/quillutils";
|
||||
|
@ -17,7 +19,9 @@ import {
|
|||
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";
|
||||
|
@ -27,10 +31,22 @@ type ReferenceListProps = {
|
|||
editor: any;
|
||||
lng: string;
|
||||
};
|
||||
//引用转换
|
||||
import Cite from "citation-js";
|
||||
|
||||
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("");
|
||||
|
@ -43,6 +59,7 @@ function ReferenceList({ editor, lng }: ReferenceListProps) {
|
|||
(state) => state.state.paperNumberRedux
|
||||
);
|
||||
const isVip = useAppSelector((state) => state.state.isVip);
|
||||
const citationStyle = useAppSelector((state) => state.state.citationStyle);
|
||||
//supabase
|
||||
const supabase = createClient();
|
||||
|
||||
|
@ -95,6 +112,44 @@ function ReferenceList({ editor, lng }: ReferenceListProps) {
|
|||
}
|
||||
}, [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=" mx-auto p-4">
|
||||
{/* 引用列表显示区域 */}
|
||||
|
@ -106,7 +161,9 @@ function ReferenceList({ editor, lng }: ReferenceListProps) {
|
|||
<li key={index} className="mb-3 p-2 border-b">
|
||||
{/* 显示序号 */}
|
||||
<span className="font-bold mr-2">[{index + 1}].</span>
|
||||
{getFullReference(reference)}
|
||||
{/* {getFullReference(reference)} */}
|
||||
{/* 根据当前风格渲染引用 */}
|
||||
{renderCitation(reference, citationStyle)}
|
||||
{reference.url && (
|
||||
<a
|
||||
href={reference.url}
|
||||
|
@ -133,7 +190,9 @@ function ReferenceList({ editor, lng }: ReferenceListProps) {
|
|||
</button>
|
||||
<button
|
||||
className="bg-gray-500 hover:bg-gray-700 text-white font-bold py-1 px-2 ml-2 rounded"
|
||||
onClick={() => copyToClipboard(getFullReference(reference))}
|
||||
onClick={() =>
|
||||
copyToClipboard(renderCitation(reference, citationStyle))
|
||||
}
|
||||
>
|
||||
{t("复制")}
|
||||
</button>
|
||||
|
@ -218,7 +277,9 @@ function ReferenceList({ editor, lng }: ReferenceListProps) {
|
|||
<button
|
||||
className="bg-gray-300 hover:bg-gray-400 text-black font-bold py-2 px-4 rounded "
|
||||
type="button"
|
||||
onClick={() => copyToClipboard(getAllFullReferences(references))}
|
||||
onClick={() =>
|
||||
copyToClipboard(getAllFullReferences(references, citationStyle))
|
||||
}
|
||||
>
|
||||
{t("复制所有引用")}
|
||||
</button>
|
||||
|
@ -230,6 +291,29 @@ function ReferenceList({ editor, lng }: ReferenceListProps) {
|
|||
>
|
||||
{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>
|
||||
|
|
|
@ -7,31 +7,63 @@ import {
|
|||
setUpsreamUrl,
|
||||
setSystemPrompt,
|
||||
} from "@/app/store/slices/authSlice";
|
||||
import { setIsJumpToReference } from "@/app/store/slices/stateSlice";
|
||||
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 = ({ lng }: { lng: string }) => {
|
||||
//i18n
|
||||
const { t } = useTranslation(lng);
|
||||
const CONFIG_OPTIONS = [
|
||||
{
|
||||
name: t("configurations.cocopilot-gpt4"),
|
||||
apiKey: "_pXVxLPBzcvCjSvG0Mv4K7G9ffw3xsM2ZKolZ",
|
||||
upstreamUrl: "https://proxy.cocopilot.org",
|
||||
},
|
||||
// {
|
||||
// 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.caifree"),
|
||||
apiKey: "sk-aiHrrRLYUUelHstX69E9484509254dBf92061d6744FfFaD1",
|
||||
upstreamUrl: "https://one.caifree.com",
|
||||
name: t("configurations.oneapi"),
|
||||
apiKey: "sk-GHuPUV6ERD8wVmmr36FeB8D809D34d93Bb857c009f6aF9Fe",
|
||||
upstreamUrl: "https://one.paperai.life",
|
||||
},
|
||||
{
|
||||
name: t("configurations.custom"),
|
||||
|
@ -48,15 +80,29 @@ const Settings = ({ lng }: { lng: string }) => {
|
|||
const isJumpToReference = useAppSelector(
|
||||
(state) => state.state.isJumpToReference
|
||||
);
|
||||
const isEvaluateTopicMatch = useAppSelector(
|
||||
(state) => state.state.isEvaluateTopicMatch
|
||||
);
|
||||
//state
|
||||
const [userConfigNumber, setUserConfigNumber] = useLocalStorage(
|
||||
"userConfigNumber",
|
||||
"2"
|
||||
);
|
||||
|
||||
const toggleSwitch = () => {
|
||||
dispatch(setIsJumpToReference(!isJumpToReference));
|
||||
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 rounded overflow-hidden shadow-lg bg-blue-gray-100 z-1000 mx-auto ">
|
||||
<h1 className="font-bold text-3xl">settings</h1>
|
||||
|
@ -106,6 +152,7 @@ const Settings = ({ lng }: { lng: string }) => {
|
|||
<input
|
||||
id="api-key"
|
||||
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"
|
||||
|
@ -148,7 +195,11 @@ const Settings = ({ lng }: { lng: string }) => {
|
|||
type="checkbox"
|
||||
className="sr-only peer"
|
||||
checked={isJumpToReference}
|
||||
onChange={toggleSwitch}
|
||||
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
|
||||
|
@ -158,6 +209,26 @@ const Settings = ({ lng }: { lng: string }) => {
|
|||
></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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
"use client";
|
||||
import { insertUserProfile } from "@/utils/supabase/supabaseutils";
|
||||
import { createClient } from "@/utils/supabase/client";
|
||||
export function SignInGitHub() {
|
||||
const signInWithGithub = async () => {
|
||||
const supabase = createClient();
|
||||
const { data, error } = await supabase.auth.signInWithOAuth({
|
||||
provider: "github",
|
||||
});
|
||||
if (error) {
|
||||
console.error("GitHub authentication failed:", error.message);
|
||||
return; // 如果出现错误,不再继续执行
|
||||
}
|
||||
//profiles表 插入用户信息
|
||||
await insertUserProfile(data, supabase);
|
||||
};
|
||||
return (
|
||||
<button
|
||||
className="bg-black text-white rounded-md px-4 py-2 flex items-center justify-center"
|
||||
onClick={signInWithGithub}
|
||||
>
|
||||
<svg
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="mr-2 h-4 w-4"
|
||||
>
|
||||
<title>GitHub icon</title>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 .3a12 12 0 0 0-3.8 23.4c.6.1.8-.3.8-.6v-2C8.7 21 8 20 8 20c-1.1 0-1.8-.9-1.8-.9-.6-1-.1-1.8.1-2 .7-.6 1.8-.4 2.7.1.1-.5.3-.9.5-1.1-2.3-.3-4.7-1.1-4.7-5 0-1.1.4-2 1-2.7 0-1 .1-2 .7-2.7 0 0 .9-.3 2.8 1a9.8 9.8 0 0 1 5.2 0c1.9-1.3 2.8-1 2.8-1 .6.7.7 1.7.7 2.7.7.7 1 1.6 1 2.7 0 3.9-2.4 4.7-4.7 5 .3.2.6.7.6 1.4v2.1c0 .3.2.7.8.6A12 12 0 0 0 12 .3"
|
||||
/>
|
||||
</svg>
|
||||
Sign In with GitHub
|
||||
</button>
|
||||
);
|
||||
}
|
70
components/SignInWithProvider.tsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -1,8 +1,5 @@
|
|||
import { Transforms } from "slate";
|
||||
import { Editor } from "slate";
|
||||
import Quill from "quill";
|
||||
|
||||
import { extractText } from "@/utils/others/slateutils";
|
||||
import {
|
||||
updateBracketNumbersInDeltaKeepSelection,
|
||||
convertToSuperscript,
|
||||
|
@ -23,12 +20,14 @@ function isValidApiKey(apiKey: string) {
|
|||
|
||||
const sendMessageToOpenAI = async (
|
||||
content: string,
|
||||
editor: Quill,
|
||||
editor: Quill | null,
|
||||
selectedModel: string,
|
||||
apiKey: string,
|
||||
upsreamUrl: string,
|
||||
prompt: string,
|
||||
cursorPosition: number
|
||||
cursorPosition: number | null,
|
||||
useEditorFlag = true, // 新增的标志,用于决定操作
|
||||
signal: AbortSignal
|
||||
) => {
|
||||
//识别应该使用的模型
|
||||
let model = selectedModel;
|
||||
|
@ -36,6 +35,7 @@ const sendMessageToOpenAI = async (
|
|||
// 设置API请求参数
|
||||
const requestOptions = {
|
||||
method: "POST",
|
||||
signal: signal,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
// "Upstream-Url": upsreamUrl,
|
||||
|
@ -47,7 +47,7 @@ const sendMessageToOpenAI = async (
|
|||
},
|
||||
body: JSON.stringify({
|
||||
model: model,
|
||||
stream: true,
|
||||
stream: useEditorFlag, // 根据标志确定是否使用streaming
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
|
@ -74,80 +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 || !response.body) {
|
||||
throw new 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();
|
||||
//开始前先进行换行
|
||||
// editor.focus();
|
||||
editor.insertText(editor.getSelection(true).index, "\n");
|
||||
await processResult(reader, decoder, editor);
|
||||
//搜索是否有相同的括号编号,如果有相同的则删除到只剩一个
|
||||
convertToSuperscript(editor);
|
||||
deleteSameBracketNumber(editor, cursorPosition);
|
||||
updateBracketNumbersInDeltaKeepSelection(editor);
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
// 如果有响应,返回响应的原始内容
|
||||
if (response) {
|
||||
const rawResponse = await response.text();
|
||||
throw new Error(`请求发生错误: ${error}, 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 + "/v1/chat/completions",
|
||||
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) {
|
||||
|
@ -179,8 +158,8 @@ async function processResult(reader, decoder, editor) {
|
|||
// 处理 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.focus();
|
||||
|
@ -189,13 +168,6 @@ async function processResult(reader, decoder, editor) {
|
|||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// console.error(
|
||||
// "there is a error in parse JSON object:",
|
||||
// jsonStr,
|
||||
// "error reason",
|
||||
// error
|
||||
// );
|
||||
// break;
|
||||
throw new Error(`
|
||||
there is a error in parse JSON object: ${jsonStr},
|
||||
error reason: ${error}`);
|
||||
|
@ -207,4 +179,4 @@ async function processResult(reader, decoder, editor) {
|
|||
}
|
||||
}
|
||||
|
||||
export { getTopicFromAI, sendMessageToOpenAI };
|
||||
export { sendMessageToOpenAI };
|
||||
|
|
11
docker-compose.yml
Normal 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 # 容器退出时重启策略
|
268
docs/.vitepress/cache/deps/@theme_index.js
vendored
|
@ -1,268 +0,0 @@
|
|||
import {
|
||||
useMediaQuery
|
||||
} from "./chunk-DN3Q4TEW.js";
|
||||
import {
|
||||
computed,
|
||||
ref,
|
||||
shallowRef,
|
||||
watch
|
||||
} from "./chunk-456JUNPJ.js";
|
||||
|
||||
// node_modules/vitepress/dist/client/theme-default/index.js
|
||||
import "C:/git-program/paper-ai/paperai-docs/node_modules/vitepress/dist/client/theme-default/styles/fonts.css";
|
||||
|
||||
// node_modules/vitepress/dist/client/theme-default/without-fonts.js
|
||||
import "C:/git-program/paper-ai/paperai-docs/node_modules/vitepress/dist/client/theme-default/styles/vars.css";
|
||||
import "C:/git-program/paper-ai/paperai-docs/node_modules/vitepress/dist/client/theme-default/styles/base.css";
|
||||
import "C:/git-program/paper-ai/paperai-docs/node_modules/vitepress/dist/client/theme-default/styles/utils.css";
|
||||
import "C:/git-program/paper-ai/paperai-docs/node_modules/vitepress/dist/client/theme-default/styles/components/custom-block.css";
|
||||
import "C:/git-program/paper-ai/paperai-docs/node_modules/vitepress/dist/client/theme-default/styles/components/vp-code.css";
|
||||
import "C:/git-program/paper-ai/paperai-docs/node_modules/vitepress/dist/client/theme-default/styles/components/vp-code-group.css";
|
||||
import "C:/git-program/paper-ai/paperai-docs/node_modules/vitepress/dist/client/theme-default/styles/components/vp-doc.css";
|
||||
import "C:/git-program/paper-ai/paperai-docs/node_modules/vitepress/dist/client/theme-default/styles/components/vp-sponsor.css";
|
||||
import VPBadge from "C:/git-program/paper-ai/paperai-docs/node_modules/vitepress/dist/client/theme-default/components/VPBadge.vue";
|
||||
import Layout from "C:/git-program/paper-ai/paperai-docs/node_modules/vitepress/dist/client/theme-default/Layout.vue";
|
||||
import { default as default2 } from "C:/git-program/paper-ai/paperai-docs/node_modules/vitepress/dist/client/theme-default/components/VPBadge.vue";
|
||||
import { default as default3 } from "C:/git-program/paper-ai/paperai-docs/node_modules/vitepress/dist/client/theme-default/components/VPImage.vue";
|
||||
import { default as default4 } from "C:/git-program/paper-ai/paperai-docs/node_modules/vitepress/dist/client/theme-default/components/VPButton.vue";
|
||||
import { default as default5 } from "C:/git-program/paper-ai/paperai-docs/node_modules/vitepress/dist/client/theme-default/components/VPHomeHero.vue";
|
||||
import { default as default6 } from "C:/git-program/paper-ai/paperai-docs/node_modules/vitepress/dist/client/theme-default/components/VPHomeFeatures.vue";
|
||||
import { default as default7 } from "C:/git-program/paper-ai/paperai-docs/node_modules/vitepress/dist/client/theme-default/components/VPHomeSponsors.vue";
|
||||
import { default as default8 } from "C:/git-program/paper-ai/paperai-docs/node_modules/vitepress/dist/client/theme-default/components/VPDocAsideSponsors.vue";
|
||||
import { default as default9 } from "C:/git-program/paper-ai/paperai-docs/node_modules/vitepress/dist/client/theme-default/components/VPSponsors.vue";
|
||||
import { default as default10 } from "C:/git-program/paper-ai/paperai-docs/node_modules/vitepress/dist/client/theme-default/components/VPTeamPage.vue";
|
||||
import { default as default11 } from "C:/git-program/paper-ai/paperai-docs/node_modules/vitepress/dist/client/theme-default/components/VPTeamPageTitle.vue";
|
||||
import { default as default12 } from "C:/git-program/paper-ai/paperai-docs/node_modules/vitepress/dist/client/theme-default/components/VPTeamPageSection.vue";
|
||||
import { default as default13 } from "C:/git-program/paper-ai/paperai-docs/node_modules/vitepress/dist/client/theme-default/components/VPTeamMembers.vue";
|
||||
|
||||
// node_modules/vitepress/dist/client/shared.js
|
||||
var inBrowser = typeof document !== "undefined";
|
||||
|
||||
// node_modules/vitepress/dist/client/theme-default/support/utils.js
|
||||
import { withBase } from "vitepress";
|
||||
|
||||
// node_modules/vitepress/dist/client/theme-default/composables/data.js
|
||||
import { useData as useData$ } from "vitepress";
|
||||
var useData = useData$;
|
||||
|
||||
// node_modules/vitepress/dist/client/theme-default/support/utils.js
|
||||
function ensureStartingSlash(path) {
|
||||
return /^\//.test(path) ? path : `/${path}`;
|
||||
}
|
||||
|
||||
// node_modules/vitepress/dist/client/theme-default/support/sidebar.js
|
||||
function getSidebar(_sidebar, path) {
|
||||
if (Array.isArray(_sidebar))
|
||||
return addBase(_sidebar);
|
||||
if (_sidebar == null)
|
||||
return [];
|
||||
path = ensureStartingSlash(path);
|
||||
const dir = Object.keys(_sidebar).sort((a, b) => {
|
||||
return b.split("/").length - a.split("/").length;
|
||||
}).find((dir2) => {
|
||||
return path.startsWith(ensureStartingSlash(dir2));
|
||||
});
|
||||
const sidebar = dir ? _sidebar[dir] : [];
|
||||
return Array.isArray(sidebar) ? addBase(sidebar) : addBase(sidebar.items, sidebar.base);
|
||||
}
|
||||
function getSidebarGroups(sidebar) {
|
||||
const groups = [];
|
||||
let lastGroupIndex = 0;
|
||||
for (const index in sidebar) {
|
||||
const item = sidebar[index];
|
||||
if (item.items) {
|
||||
lastGroupIndex = groups.push(item);
|
||||
continue;
|
||||
}
|
||||
if (!groups[lastGroupIndex]) {
|
||||
groups.push({ items: [] });
|
||||
}
|
||||
groups[lastGroupIndex].items.push(item);
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
function addBase(items, _base) {
|
||||
return [...items].map((_item) => {
|
||||
const item = { ..._item };
|
||||
const base = item.base || _base;
|
||||
if (base && item.link)
|
||||
item.link = base + item.link;
|
||||
if (item.items)
|
||||
item.items = addBase(item.items, base);
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
// node_modules/vitepress/dist/client/theme-default/composables/hash.js
|
||||
var hashRef = ref(inBrowser ? location.hash : "");
|
||||
if (inBrowser) {
|
||||
window.addEventListener("hashchange", () => {
|
||||
hashRef.value = location.hash;
|
||||
});
|
||||
}
|
||||
|
||||
// node_modules/vitepress/dist/client/theme-default/composables/sidebar.js
|
||||
function useSidebar() {
|
||||
const { frontmatter, page, theme: theme2 } = useData();
|
||||
const is960 = useMediaQuery("(min-width: 960px)");
|
||||
const isOpen = ref(false);
|
||||
const _sidebar = computed(() => {
|
||||
const sidebarConfig = theme2.value.sidebar;
|
||||
const relativePath = page.value.relativePath;
|
||||
return sidebarConfig ? getSidebar(sidebarConfig, relativePath) : [];
|
||||
});
|
||||
const sidebar = ref(_sidebar.value);
|
||||
watch(_sidebar, (next, prev) => {
|
||||
if (JSON.stringify(next) !== JSON.stringify(prev))
|
||||
sidebar.value = _sidebar.value;
|
||||
});
|
||||
const hasSidebar = computed(() => {
|
||||
return frontmatter.value.sidebar !== false && sidebar.value.length > 0 && frontmatter.value.layout !== "home";
|
||||
});
|
||||
const leftAside = computed(() => {
|
||||
if (hasAside)
|
||||
return frontmatter.value.aside == null ? theme2.value.aside === "left" : frontmatter.value.aside === "left";
|
||||
return false;
|
||||
});
|
||||
const hasAside = computed(() => {
|
||||
if (frontmatter.value.layout === "home")
|
||||
return false;
|
||||
if (frontmatter.value.aside != null)
|
||||
return !!frontmatter.value.aside;
|
||||
return theme2.value.aside !== false;
|
||||
});
|
||||
const isSidebarEnabled = computed(() => hasSidebar.value && is960.value);
|
||||
const sidebarGroups = computed(() => {
|
||||
return hasSidebar.value ? getSidebarGroups(sidebar.value) : [];
|
||||
});
|
||||
function open() {
|
||||
isOpen.value = true;
|
||||
}
|
||||
function close() {
|
||||
isOpen.value = false;
|
||||
}
|
||||
function toggle() {
|
||||
isOpen.value ? close() : open();
|
||||
}
|
||||
return {
|
||||
isOpen,
|
||||
sidebar,
|
||||
sidebarGroups,
|
||||
hasSidebar,
|
||||
hasAside,
|
||||
leftAside,
|
||||
isSidebarEnabled,
|
||||
open,
|
||||
close,
|
||||
toggle
|
||||
};
|
||||
}
|
||||
|
||||
// node_modules/vitepress/dist/client/theme-default/composables/local-nav.js
|
||||
import { onContentUpdated } from "vitepress";
|
||||
|
||||
// node_modules/vitepress/dist/client/theme-default/composables/outline.js
|
||||
import { getScrollOffset } from "vitepress";
|
||||
var resolvedHeaders = [];
|
||||
function getHeaders(range) {
|
||||
const headers = [
|
||||
...document.querySelectorAll(".VPDoc :where(h1,h2,h3,h4,h5,h6)")
|
||||
].filter((el) => el.id && el.hasChildNodes()).map((el) => {
|
||||
const level = Number(el.tagName[1]);
|
||||
return {
|
||||
element: el,
|
||||
title: serializeHeader(el),
|
||||
link: "#" + el.id,
|
||||
level
|
||||
};
|
||||
});
|
||||
return resolveHeaders(headers, range);
|
||||
}
|
||||
function serializeHeader(h) {
|
||||
let ret = "";
|
||||
for (const node of h.childNodes) {
|
||||
if (node.nodeType === 1) {
|
||||
if (node.classList.contains("VPBadge") || node.classList.contains("header-anchor") || node.classList.contains("ignore-header")) {
|
||||
continue;
|
||||
}
|
||||
ret += node.textContent;
|
||||
} else if (node.nodeType === 3) {
|
||||
ret += node.textContent;
|
||||
}
|
||||
}
|
||||
return ret.trim();
|
||||
}
|
||||
function resolveHeaders(headers, range) {
|
||||
if (range === false) {
|
||||
return [];
|
||||
}
|
||||
const levelsRange = (typeof range === "object" && !Array.isArray(range) ? range.level : range) || 2;
|
||||
const [high, low] = typeof levelsRange === "number" ? [levelsRange, levelsRange] : levelsRange === "deep" ? [2, 6] : levelsRange;
|
||||
headers = headers.filter((h) => h.level >= high && h.level <= low);
|
||||
resolvedHeaders.length = 0;
|
||||
for (const { element, link } of headers) {
|
||||
resolvedHeaders.push({ element, link });
|
||||
}
|
||||
const ret = [];
|
||||
outer:
|
||||
for (let i = 0; i < headers.length; i++) {
|
||||
const cur = headers[i];
|
||||
if (i === 0) {
|
||||
ret.push(cur);
|
||||
} else {
|
||||
for (let j = i - 1; j >= 0; j--) {
|
||||
const prev = headers[j];
|
||||
if (prev.level < cur.level) {
|
||||
;
|
||||
(prev.children || (prev.children = [])).push(cur);
|
||||
continue outer;
|
||||
}
|
||||
}
|
||||
ret.push(cur);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
// node_modules/vitepress/dist/client/theme-default/composables/local-nav.js
|
||||
function useLocalNav() {
|
||||
const { theme: theme2, frontmatter } = useData();
|
||||
const headers = shallowRef([]);
|
||||
const hasLocalNav = computed(() => {
|
||||
return headers.value.length > 0;
|
||||
});
|
||||
onContentUpdated(() => {
|
||||
headers.value = getHeaders(frontmatter.value.outline ?? theme2.value.outline);
|
||||
});
|
||||
return {
|
||||
headers,
|
||||
hasLocalNav
|
||||
};
|
||||
}
|
||||
|
||||
// node_modules/vitepress/dist/client/theme-default/without-fonts.js
|
||||
var theme = {
|
||||
Layout,
|
||||
enhanceApp: ({ app }) => {
|
||||
app.component("Badge", VPBadge);
|
||||
}
|
||||
};
|
||||
var without_fonts_default = theme;
|
||||
export {
|
||||
default2 as VPBadge,
|
||||
default4 as VPButton,
|
||||
default8 as VPDocAsideSponsors,
|
||||
default6 as VPHomeFeatures,
|
||||
default5 as VPHomeHero,
|
||||
default7 as VPHomeSponsors,
|
||||
default3 as VPImage,
|
||||
default9 as VPSponsors,
|
||||
default13 as VPTeamMembers,
|
||||
default10 as VPTeamPage,
|
||||
default12 as VPTeamPageSection,
|
||||
default11 as VPTeamPageTitle,
|
||||
without_fonts_default as default,
|
||||
useLocalNav,
|
||||
useSidebar
|
||||
};
|
||||
//# sourceMappingURL=@theme_index.js.map
|
40
docs/.vitepress/cache/deps/_metadata.json
vendored
|
@ -1,40 +0,0 @@
|
|||
{
|
||||
"hash": "8c371e85",
|
||||
"configHash": "441e0395",
|
||||
"lockfileHash": "9cf9febc",
|
||||
"browserHash": "c76444d4",
|
||||
"optimized": {
|
||||
"vue": {
|
||||
"src": "../../../node_modules/vue/dist/vue.runtime.esm-bundler.js",
|
||||
"file": "vue.js",
|
||||
"fileHash": "263297c9",
|
||||
"needsInterop": false
|
||||
},
|
||||
"vitepress > @vue/devtools-api": {
|
||||
"src": "../../../node_modules/@vue/devtools-api/dist/index.js",
|
||||
"file": "vitepress___@vue_devtools-api.js",
|
||||
"fileHash": "483a8161",
|
||||
"needsInterop": false
|
||||
},
|
||||
"vitepress > @vueuse/core": {
|
||||
"src": "../../../node_modules/@vueuse/core/index.mjs",
|
||||
"file": "vitepress___@vueuse_core.js",
|
||||
"fileHash": "5dcb9da3",
|
||||
"needsInterop": false
|
||||
},
|
||||
"@theme/index": {
|
||||
"src": "../../../node_modules/vitepress/dist/client/theme-default/index.js",
|
||||
"file": "@theme_index.js",
|
||||
"fileHash": "9a4b4951",
|
||||
"needsInterop": false
|
||||
}
|
||||
},
|
||||
"chunks": {
|
||||
"chunk-DN3Q4TEW": {
|
||||
"file": "chunk-DN3Q4TEW.js"
|
||||
},
|
||||
"chunk-456JUNPJ": {
|
||||
"file": "chunk-456JUNPJ.js"
|
||||
}
|
||||
}
|
||||
}
|
11388
docs/.vitepress/cache/deps/chunk-456JUNPJ.js
vendored
8965
docs/.vitepress/cache/deps/chunk-DN3Q4TEW.js
vendored
3
docs/.vitepress/cache/deps/package.json
vendored
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"type": "module"
|
||||
}
|
|
@ -1,563 +0,0 @@
|
|||
import {
|
||||
DefaultMagicKeysAliasMap,
|
||||
StorageSerializers,
|
||||
TransitionPresets,
|
||||
assert,
|
||||
breakpointsAntDesign,
|
||||
breakpointsBootstrapV5,
|
||||
breakpointsMasterCss,
|
||||
breakpointsPrimeFlex,
|
||||
breakpointsQuasar,
|
||||
breakpointsSematic,
|
||||
breakpointsTailwind,
|
||||
breakpointsVuetify,
|
||||
bypassFilter,
|
||||
camelize,
|
||||
clamp,
|
||||
cloneFnJSON,
|
||||
computedAsync,
|
||||
computedEager,
|
||||
computedInject,
|
||||
computedWithControl,
|
||||
containsProp,
|
||||
controlledRef,
|
||||
createEventHook,
|
||||
createFetch,
|
||||
createFilterWrapper,
|
||||
createGlobalState,
|
||||
createInjectionState,
|
||||
createReusableTemplate,
|
||||
createSharedComposable,
|
||||
createSingletonPromise,
|
||||
createTemplatePromise,
|
||||
createUnrefFn,
|
||||
customStorageEventName,
|
||||
debounceFilter,
|
||||
defaultDocument,
|
||||
defaultLocation,
|
||||
defaultNavigator,
|
||||
defaultWindow,
|
||||
directiveHooks,
|
||||
executeTransition,
|
||||
extendRef,
|
||||
formatDate,
|
||||
formatTimeAgo,
|
||||
get,
|
||||
getLifeCycleTarget,
|
||||
getSSRHandler,
|
||||
hasOwn,
|
||||
hyphenate,
|
||||
identity,
|
||||
increaseWithUnit,
|
||||
injectLocal,
|
||||
invoke,
|
||||
isClient,
|
||||
isDef,
|
||||
isDefined,
|
||||
isIOS,
|
||||
isObject,
|
||||
isWorker,
|
||||
makeDestructurable,
|
||||
mapGamepadToXbox360Controller,
|
||||
noop,
|
||||
normalizeDate,
|
||||
notNullish,
|
||||
now,
|
||||
objectEntries,
|
||||
objectOmit,
|
||||
objectPick,
|
||||
onClickOutside,
|
||||
onKeyDown,
|
||||
onKeyPressed,
|
||||
onKeyStroke,
|
||||
onKeyUp,
|
||||
onLongPress,
|
||||
onStartTyping,
|
||||
pausableFilter,
|
||||
promiseTimeout,
|
||||
provideLocal,
|
||||
rand,
|
||||
reactify,
|
||||
reactifyObject,
|
||||
reactiveComputed,
|
||||
reactiveOmit,
|
||||
reactivePick,
|
||||
refAutoReset,
|
||||
refDebounced,
|
||||
refDefault,
|
||||
refThrottled,
|
||||
refWithControl,
|
||||
resolveRef,
|
||||
resolveUnref,
|
||||
set,
|
||||
setSSRHandler,
|
||||
syncRef,
|
||||
syncRefs,
|
||||
templateRef,
|
||||
throttleFilter,
|
||||
timestamp,
|
||||
toReactive,
|
||||
toRef,
|
||||
toRefs,
|
||||
toValue,
|
||||
tryOnBeforeMount,
|
||||
tryOnBeforeUnmount,
|
||||
tryOnMounted,
|
||||
tryOnScopeDispose,
|
||||
tryOnUnmounted,
|
||||
unrefElement,
|
||||
until,
|
||||
useActiveElement,
|
||||
useAnimate,
|
||||
useArrayDifference,
|
||||
useArrayEvery,
|
||||
useArrayFilter,
|
||||
useArrayFind,
|
||||
useArrayFindIndex,
|
||||
useArrayFindLast,
|
||||
useArrayIncludes,
|
||||
useArrayJoin,
|
||||
useArrayMap,
|
||||
useArrayReduce,
|
||||
useArraySome,
|
||||
useArrayUnique,
|
||||
useAsyncQueue,
|
||||
useAsyncState,
|
||||
useBase64,
|
||||
useBattery,
|
||||
useBluetooth,
|
||||
useBreakpoints,
|
||||
useBroadcastChannel,
|
||||
useBrowserLocation,
|
||||
useCached,
|
||||
useClipboard,
|
||||
useClipboardItems,
|
||||
useCloned,
|
||||
useColorMode,
|
||||
useConfirmDialog,
|
||||
useCounter,
|
||||
useCssVar,
|
||||
useCurrentElement,
|
||||
useCycleList,
|
||||
useDark,
|
||||
useDateFormat,
|
||||
useDebounceFn,
|
||||
useDebouncedRefHistory,
|
||||
useDeviceMotion,
|
||||
useDeviceOrientation,
|
||||
useDevicePixelRatio,
|
||||
useDevicesList,
|
||||
useDisplayMedia,
|
||||
useDocumentVisibility,
|
||||
useDraggable,
|
||||
useDropZone,
|
||||
useElementBounding,
|
||||
useElementByPoint,
|
||||
useElementHover,
|
||||
useElementSize,
|
||||
useElementVisibility,
|
||||
useEventBus,
|
||||
useEventListener,
|
||||
useEventSource,
|
||||
useEyeDropper,
|
||||
useFavicon,
|
||||
useFetch,
|
||||
useFileDialog,
|
||||
useFileSystemAccess,
|
||||
useFocus,
|
||||
useFocusWithin,
|
||||
useFps,
|
||||
useFullscreen,
|
||||
useGamepad,
|
||||
useGeolocation,
|
||||
useIdle,
|
||||
useImage,
|
||||
useInfiniteScroll,
|
||||
useIntersectionObserver,
|
||||
useInterval,
|
||||
useIntervalFn,
|
||||
useKeyModifier,
|
||||
useLastChanged,
|
||||
useLocalStorage,
|
||||
useMagicKeys,
|
||||
useManualRefHistory,
|
||||
useMediaControls,
|
||||
useMediaQuery,
|
||||
useMemoize,
|
||||
useMemory,
|
||||
useMounted,
|
||||
useMouse,
|
||||
useMouseInElement,
|
||||
useMousePressed,
|
||||
useMutationObserver,
|
||||
useNavigatorLanguage,
|
||||
useNetwork,
|
||||
useNow,
|
||||
useObjectUrl,
|
||||
useOffsetPagination,
|
||||
useOnline,
|
||||
usePageLeave,
|
||||
useParallax,
|
||||
useParentElement,
|
||||
usePerformanceObserver,
|
||||
usePermission,
|
||||
usePointer,
|
||||
usePointerLock,
|
||||
usePointerSwipe,
|
||||
usePreferredColorScheme,
|
||||
usePreferredContrast,
|
||||
usePreferredDark,
|
||||
usePreferredLanguages,
|
||||
usePreferredReducedMotion,
|
||||
usePrevious,
|
||||
useRafFn,
|
||||
useRefHistory,
|
||||
useResizeObserver,
|
||||
useScreenOrientation,
|
||||
useScreenSafeArea,
|
||||
useScriptTag,
|
||||
useScroll,
|
||||
useScrollLock,
|
||||
useSessionStorage,
|
||||
useShare,
|
||||
useSorted,
|
||||
useSpeechRecognition,
|
||||
useSpeechSynthesis,
|
||||
useStepper,
|
||||
useStorage,
|
||||
useStorageAsync,
|
||||
useStyleTag,
|
||||
useSupported,
|
||||
useSwipe,
|
||||
useTemplateRefsList,
|
||||
useTextDirection,
|
||||
useTextSelection,
|
||||
useTextareaAutosize,
|
||||
useThrottleFn,
|
||||
useThrottledRefHistory,
|
||||
useTimeAgo,
|
||||
useTimeout,
|
||||
useTimeoutFn,
|
||||
useTimeoutPoll,
|
||||
useTimestamp,
|
||||
useTitle,
|
||||
useToNumber,
|
||||
useToString,
|
||||
useToggle,
|
||||
useTransition,
|
||||
useUrlSearchParams,
|
||||
useUserMedia,
|
||||
useVModel,
|
||||
useVModels,
|
||||
useVibrate,
|
||||
useVirtualList,
|
||||
useWakeLock,
|
||||
useWebNotification,
|
||||
useWebSocket,
|
||||
useWebWorker,
|
||||
useWebWorkerFn,
|
||||
useWindowFocus,
|
||||
useWindowScroll,
|
||||
useWindowSize,
|
||||
watchArray,
|
||||
watchAtMost,
|
||||
watchDebounced,
|
||||
watchDeep,
|
||||
watchIgnorable,
|
||||
watchImmediate,
|
||||
watchOnce,
|
||||
watchPausable,
|
||||
watchThrottled,
|
||||
watchTriggerable,
|
||||
watchWithFilter,
|
||||
whenever
|
||||
} from "./chunk-DN3Q4TEW.js";
|
||||
import "./chunk-456JUNPJ.js";
|
||||
export {
|
||||
DefaultMagicKeysAliasMap,
|
||||
StorageSerializers,
|
||||
TransitionPresets,
|
||||
assert,
|
||||
computedAsync as asyncComputed,
|
||||
refAutoReset as autoResetRef,
|
||||
breakpointsAntDesign,
|
||||
breakpointsBootstrapV5,
|
||||
breakpointsMasterCss,
|
||||
breakpointsPrimeFlex,
|
||||
breakpointsQuasar,
|
||||
breakpointsSematic,
|
||||
breakpointsTailwind,
|
||||
breakpointsVuetify,
|
||||
bypassFilter,
|
||||
camelize,
|
||||
clamp,
|
||||
cloneFnJSON,
|
||||
computedAsync,
|
||||
computedEager,
|
||||
computedInject,
|
||||
computedWithControl,
|
||||
containsProp,
|
||||
computedWithControl as controlledComputed,
|
||||
controlledRef,
|
||||
createEventHook,
|
||||
createFetch,
|
||||
createFilterWrapper,
|
||||
createGlobalState,
|
||||
createInjectionState,
|
||||
reactify as createReactiveFn,
|
||||
createReusableTemplate,
|
||||
createSharedComposable,
|
||||
createSingletonPromise,
|
||||
createTemplatePromise,
|
||||
createUnrefFn,
|
||||
customStorageEventName,
|
||||
debounceFilter,
|
||||
refDebounced as debouncedRef,
|
||||
watchDebounced as debouncedWatch,
|
||||
defaultDocument,
|
||||
defaultLocation,
|
||||
defaultNavigator,
|
||||
defaultWindow,
|
||||
directiveHooks,
|
||||
computedEager as eagerComputed,
|
||||
executeTransition,
|
||||
extendRef,
|
||||
formatDate,
|
||||
formatTimeAgo,
|
||||
get,
|
||||
getLifeCycleTarget,
|
||||
getSSRHandler,
|
||||
hasOwn,
|
||||
hyphenate,
|
||||
identity,
|
||||
watchIgnorable as ignorableWatch,
|
||||
increaseWithUnit,
|
||||
injectLocal,
|
||||
invoke,
|
||||
isClient,
|
||||
isDef,
|
||||
isDefined,
|
||||
isIOS,
|
||||
isObject,
|
||||
isWorker,
|
||||
makeDestructurable,
|
||||
mapGamepadToXbox360Controller,
|
||||
noop,
|
||||
normalizeDate,
|
||||
notNullish,
|
||||
now,
|
||||
objectEntries,
|
||||
objectOmit,
|
||||
objectPick,
|
||||
onClickOutside,
|
||||
onKeyDown,
|
||||
onKeyPressed,
|
||||
onKeyStroke,
|
||||
onKeyUp,
|
||||
onLongPress,
|
||||
onStartTyping,
|
||||
pausableFilter,
|
||||
watchPausable as pausableWatch,
|
||||
promiseTimeout,
|
||||
provideLocal,
|
||||
rand,
|
||||
reactify,
|
||||
reactifyObject,
|
||||
reactiveComputed,
|
||||
reactiveOmit,
|
||||
reactivePick,
|
||||
refAutoReset,
|
||||
refDebounced,
|
||||
refDefault,
|
||||
refThrottled,
|
||||
refWithControl,
|
||||
resolveRef,
|
||||
resolveUnref,
|
||||
set,
|
||||
setSSRHandler,
|
||||
syncRef,
|
||||
syncRefs,
|
||||
templateRef,
|
||||
throttleFilter,
|
||||
refThrottled as throttledRef,
|
||||
watchThrottled as throttledWatch,
|
||||
timestamp,
|
||||
toReactive,
|
||||
toRef,
|
||||
toRefs,
|
||||
toValue,
|
||||
tryOnBeforeMount,
|
||||
tryOnBeforeUnmount,
|
||||
tryOnMounted,
|
||||
tryOnScopeDispose,
|
||||
tryOnUnmounted,
|
||||
unrefElement,
|
||||
until,
|
||||
useActiveElement,
|
||||
useAnimate,
|
||||
useArrayDifference,
|
||||
useArrayEvery,
|
||||
useArrayFilter,
|
||||
useArrayFind,
|
||||
useArrayFindIndex,
|
||||
useArrayFindLast,
|
||||
useArrayIncludes,
|
||||
useArrayJoin,
|
||||
useArrayMap,
|
||||
useArrayReduce,
|
||||
useArraySome,
|
||||
useArrayUnique,
|
||||
useAsyncQueue,
|
||||
useAsyncState,
|
||||
useBase64,
|
||||
useBattery,
|
||||
useBluetooth,
|
||||
useBreakpoints,
|
||||
useBroadcastChannel,
|
||||
useBrowserLocation,
|
||||
useCached,
|
||||
useClipboard,
|
||||
useClipboardItems,
|
||||
useCloned,
|
||||
useColorMode,
|
||||
useConfirmDialog,
|
||||
useCounter,
|
||||
useCssVar,
|
||||
useCurrentElement,
|
||||
useCycleList,
|
||||
useDark,
|
||||
useDateFormat,
|
||||
refDebounced as useDebounce,
|
||||
useDebounceFn,
|
||||
useDebouncedRefHistory,
|
||||
useDeviceMotion,
|
||||
useDeviceOrientation,
|
||||
useDevicePixelRatio,
|
||||
useDevicesList,
|
||||
useDisplayMedia,
|
||||
useDocumentVisibility,
|
||||
useDraggable,
|
||||
useDropZone,
|
||||
useElementBounding,
|
||||
useElementByPoint,
|
||||
useElementHover,
|
||||
useElementSize,
|
||||
useElementVisibility,
|
||||
useEventBus,
|
||||
useEventListener,
|
||||
useEventSource,
|
||||
useEyeDropper,
|
||||
useFavicon,
|
||||
useFetch,
|
||||
useFileDialog,
|
||||
useFileSystemAccess,
|
||||
useFocus,
|
||||
useFocusWithin,
|
||||
useFps,
|
||||
useFullscreen,
|
||||
useGamepad,
|
||||
useGeolocation,
|
||||
useIdle,
|
||||
useImage,
|
||||
useInfiniteScroll,
|
||||
useIntersectionObserver,
|
||||
useInterval,
|
||||
useIntervalFn,
|
||||
useKeyModifier,
|
||||
useLastChanged,
|
||||
useLocalStorage,
|
||||
useMagicKeys,
|
||||
useManualRefHistory,
|
||||
useMediaControls,
|
||||
useMediaQuery,
|
||||
useMemoize,
|
||||
useMemory,
|
||||
useMounted,
|
||||
useMouse,
|
||||
useMouseInElement,
|
||||
useMousePressed,
|
||||
useMutationObserver,
|
||||
useNavigatorLanguage,
|
||||
useNetwork,
|
||||
useNow,
|
||||
useObjectUrl,
|
||||
useOffsetPagination,
|
||||
useOnline,
|
||||
usePageLeave,
|
||||
useParallax,
|
||||
useParentElement,
|
||||
usePerformanceObserver,
|
||||
usePermission,
|
||||
usePointer,
|
||||
usePointerLock,
|
||||
usePointerSwipe,
|
||||
usePreferredColorScheme,
|
||||
usePreferredContrast,
|
||||
usePreferredDark,
|
||||
usePreferredLanguages,
|
||||
usePreferredReducedMotion,
|
||||
usePrevious,
|
||||
useRafFn,
|
||||
useRefHistory,
|
||||
useResizeObserver,
|
||||
useScreenOrientation,
|
||||
useScreenSafeArea,
|
||||
useScriptTag,
|
||||
useScroll,
|
||||
useScrollLock,
|
||||
useSessionStorage,
|
||||
useShare,
|
||||
useSorted,
|
||||
useSpeechRecognition,
|
||||
useSpeechSynthesis,
|
||||
useStepper,
|
||||
useStorage,
|
||||
useStorageAsync,
|
||||
useStyleTag,
|
||||
useSupported,
|
||||
useSwipe,
|
||||
useTemplateRefsList,
|
||||
useTextDirection,
|
||||
useTextSelection,
|
||||
useTextareaAutosize,
|
||||
refThrottled as useThrottle,
|
||||
useThrottleFn,
|
||||
useThrottledRefHistory,
|
||||
useTimeAgo,
|
||||
useTimeout,
|
||||
useTimeoutFn,
|
||||
useTimeoutPoll,
|
||||
useTimestamp,
|
||||
useTitle,
|
||||
useToNumber,
|
||||
useToString,
|
||||
useToggle,
|
||||
useTransition,
|
||||
useUrlSearchParams,
|
||||
useUserMedia,
|
||||
useVModel,
|
||||
useVModels,
|
||||
useVibrate,
|
||||
useVirtualList,
|
||||
useWakeLock,
|
||||
useWebNotification,
|
||||
useWebSocket,
|
||||
useWebWorker,
|
||||
useWebWorkerFn,
|
||||
useWindowFocus,
|
||||
useWindowScroll,
|
||||
useWindowSize,
|
||||
watchArray,
|
||||
watchAtMost,
|
||||
watchDebounced,
|
||||
watchDeep,
|
||||
watchIgnorable,
|
||||
watchImmediate,
|
||||
watchOnce,
|
||||
watchPausable,
|
||||
watchThrottled,
|
||||
watchTriggerable,
|
||||
watchWithFilter,
|
||||
whenever
|
||||
};
|
||||
//# sourceMappingURL=vitepress___@vueuse_core.js.map
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"version": 3,
|
||||
"sources": [],
|
||||
"sourcesContent": [],
|
||||
"mappings": "",
|
||||
"names": []
|
||||
}
|
323
docs/.vitepress/cache/deps/vue.js
vendored
|
@ -1,323 +0,0 @@
|
|||
import {
|
||||
BaseTransition,
|
||||
BaseTransitionPropsValidators,
|
||||
Comment,
|
||||
DeprecationTypes,
|
||||
EffectScope,
|
||||
ErrorCodes,
|
||||
ErrorTypeStrings,
|
||||
Fragment,
|
||||
KeepAlive,
|
||||
ReactiveEffect,
|
||||
Static,
|
||||
Suspense,
|
||||
Teleport,
|
||||
Text,
|
||||
TrackOpTypes,
|
||||
Transition,
|
||||
TransitionGroup,
|
||||
TriggerOpTypes,
|
||||
VueElement,
|
||||
assertNumber,
|
||||
callWithAsyncErrorHandling,
|
||||
callWithErrorHandling,
|
||||
camelize,
|
||||
capitalize,
|
||||
cloneVNode,
|
||||
compatUtils,
|
||||
compile,
|
||||
computed,
|
||||
createApp,
|
||||
createBaseVNode,
|
||||
createBlock,
|
||||
createCommentVNode,
|
||||
createElementBlock,
|
||||
createHydrationRenderer,
|
||||
createPropsRestProxy,
|
||||
createRenderer,
|
||||
createSSRApp,
|
||||
createSlots,
|
||||
createStaticVNode,
|
||||
createTextVNode,
|
||||
createVNode,
|
||||
customRef,
|
||||
defineAsyncComponent,
|
||||
defineComponent,
|
||||
defineCustomElement,
|
||||
defineEmits,
|
||||
defineExpose,
|
||||
defineModel,
|
||||
defineOptions,
|
||||
defineProps,
|
||||
defineSSRCustomElement,
|
||||
defineSlots,
|
||||
devtools,
|
||||
effect,
|
||||
effectScope,
|
||||
getCurrentInstance,
|
||||
getCurrentScope,
|
||||
getTransitionRawChildren,
|
||||
guardReactiveProps,
|
||||
h,
|
||||
handleError,
|
||||
hasInjectionContext,
|
||||
hydrate,
|
||||
initCustomFormatter,
|
||||
initDirectivesForSSR,
|
||||
inject,
|
||||
isMemoSame,
|
||||
isProxy,
|
||||
isReactive,
|
||||
isReadonly,
|
||||
isRef,
|
||||
isRuntimeOnly,
|
||||
isShallow,
|
||||
isVNode,
|
||||
markRaw,
|
||||
mergeDefaults,
|
||||
mergeModels,
|
||||
mergeProps,
|
||||
nextTick,
|
||||
normalizeClass,
|
||||
normalizeProps,
|
||||
normalizeStyle,
|
||||
onActivated,
|
||||
onBeforeMount,
|
||||
onBeforeUnmount,
|
||||
onBeforeUpdate,
|
||||
onDeactivated,
|
||||
onErrorCaptured,
|
||||
onMounted,
|
||||
onRenderTracked,
|
||||
onRenderTriggered,
|
||||
onScopeDispose,
|
||||
onServerPrefetch,
|
||||
onUnmounted,
|
||||
onUpdated,
|
||||
openBlock,
|
||||
popScopeId,
|
||||
provide,
|
||||
proxyRefs,
|
||||
pushScopeId,
|
||||
queuePostFlushCb,
|
||||
reactive,
|
||||
readonly,
|
||||
ref,
|
||||
registerRuntimeCompiler,
|
||||
render,
|
||||
renderList,
|
||||
renderSlot,
|
||||
resolveComponent,
|
||||
resolveDirective,
|
||||
resolveDynamicComponent,
|
||||
resolveFilter,
|
||||
resolveTransitionHooks,
|
||||
setBlockTracking,
|
||||
setDevtoolsHook,
|
||||
setTransitionHooks,
|
||||
shallowReactive,
|
||||
shallowReadonly,
|
||||
shallowRef,
|
||||
ssrContextKey,
|
||||
ssrUtils,
|
||||
stop,
|
||||
toDisplayString,
|
||||
toHandlerKey,
|
||||
toHandlers,
|
||||
toRaw,
|
||||
toRef,
|
||||
toRefs,
|
||||
toValue,
|
||||
transformVNodeArgs,
|
||||
triggerRef,
|
||||
unref,
|
||||
useAttrs,
|
||||
useCssModule,
|
||||
useCssVars,
|
||||
useModel,
|
||||
useSSRContext,
|
||||
useSlots,
|
||||
useTransitionState,
|
||||
vModelCheckbox,
|
||||
vModelDynamic,
|
||||
vModelRadio,
|
||||
vModelSelect,
|
||||
vModelText,
|
||||
vShow,
|
||||
version,
|
||||
warn,
|
||||
watch,
|
||||
watchEffect,
|
||||
watchPostEffect,
|
||||
watchSyncEffect,
|
||||
withAsyncContext,
|
||||
withCtx,
|
||||
withDefaults,
|
||||
withDirectives,
|
||||
withKeys,
|
||||
withMemo,
|
||||
withModifiers,
|
||||
withScopeId
|
||||
} from "./chunk-456JUNPJ.js";
|
||||
export {
|
||||
BaseTransition,
|
||||
BaseTransitionPropsValidators,
|
||||
Comment,
|
||||
DeprecationTypes,
|
||||
EffectScope,
|
||||
ErrorCodes,
|
||||
ErrorTypeStrings,
|
||||
Fragment,
|
||||
KeepAlive,
|
||||
ReactiveEffect,
|
||||
Static,
|
||||
Suspense,
|
||||
Teleport,
|
||||
Text,
|
||||
TrackOpTypes,
|
||||
Transition,
|
||||
TransitionGroup,
|
||||
TriggerOpTypes,
|
||||
VueElement,
|
||||
assertNumber,
|
||||
callWithAsyncErrorHandling,
|
||||
callWithErrorHandling,
|
||||
camelize,
|
||||
capitalize,
|
||||
cloneVNode,
|
||||
compatUtils,
|
||||
compile,
|
||||
computed,
|
||||
createApp,
|
||||
createBlock,
|
||||
createCommentVNode,
|
||||
createElementBlock,
|
||||
createBaseVNode as createElementVNode,
|
||||
createHydrationRenderer,
|
||||
createPropsRestProxy,
|
||||
createRenderer,
|
||||
createSSRApp,
|
||||
createSlots,
|
||||
createStaticVNode,
|
||||
createTextVNode,
|
||||
createVNode,
|
||||
customRef,
|
||||
defineAsyncComponent,
|
||||
defineComponent,
|
||||
defineCustomElement,
|
||||
defineEmits,
|
||||
defineExpose,
|
||||
defineModel,
|
||||
defineOptions,
|
||||
defineProps,
|
||||
defineSSRCustomElement,
|
||||
defineSlots,
|
||||
devtools,
|
||||
effect,
|
||||
effectScope,
|
||||
getCurrentInstance,
|
||||
getCurrentScope,
|
||||
getTransitionRawChildren,
|
||||
guardReactiveProps,
|
||||
h,
|
||||
handleError,
|
||||
hasInjectionContext,
|
||||
hydrate,
|
||||
initCustomFormatter,
|
||||
initDirectivesForSSR,
|
||||
inject,
|
||||
isMemoSame,
|
||||
isProxy,
|
||||
isReactive,
|
||||
isReadonly,
|
||||
isRef,
|
||||
isRuntimeOnly,
|
||||
isShallow,
|
||||
isVNode,
|
||||
markRaw,
|
||||
mergeDefaults,
|
||||
mergeModels,
|
||||
mergeProps,
|
||||
nextTick,
|
||||
normalizeClass,
|
||||
normalizeProps,
|
||||
normalizeStyle,
|
||||
onActivated,
|
||||
onBeforeMount,
|
||||
onBeforeUnmount,
|
||||
onBeforeUpdate,
|
||||
onDeactivated,
|
||||
onErrorCaptured,
|
||||
onMounted,
|
||||
onRenderTracked,
|
||||
onRenderTriggered,
|
||||
onScopeDispose,
|
||||
onServerPrefetch,
|
||||
onUnmounted,
|
||||
onUpdated,
|
||||
openBlock,
|
||||
popScopeId,
|
||||
provide,
|
||||
proxyRefs,
|
||||
pushScopeId,
|
||||
queuePostFlushCb,
|
||||
reactive,
|
||||
readonly,
|
||||
ref,
|
||||
registerRuntimeCompiler,
|
||||
render,
|
||||
renderList,
|
||||
renderSlot,
|
||||
resolveComponent,
|
||||
resolveDirective,
|
||||
resolveDynamicComponent,
|
||||
resolveFilter,
|
||||
resolveTransitionHooks,
|
||||
setBlockTracking,
|
||||
setDevtoolsHook,
|
||||
setTransitionHooks,
|
||||
shallowReactive,
|
||||
shallowReadonly,
|
||||
shallowRef,
|
||||
ssrContextKey,
|
||||
ssrUtils,
|
||||
stop,
|
||||
toDisplayString,
|
||||
toHandlerKey,
|
||||
toHandlers,
|
||||
toRaw,
|
||||
toRef,
|
||||
toRefs,
|
||||
toValue,
|
||||
transformVNodeArgs,
|
||||
triggerRef,
|
||||
unref,
|
||||
useAttrs,
|
||||
useCssModule,
|
||||
useCssVars,
|
||||
useModel,
|
||||
useSSRContext,
|
||||
useSlots,
|
||||
useTransitionState,
|
||||
vModelCheckbox,
|
||||
vModelDynamic,
|
||||
vModelRadio,
|
||||
vModelSelect,
|
||||
vModelText,
|
||||
vShow,
|
||||
version,
|
||||
warn,
|
||||
watch,
|
||||
watchEffect,
|
||||
watchPostEffect,
|
||||
watchSyncEffect,
|
||||
withAsyncContext,
|
||||
withCtx,
|
||||
withDefaults,
|
||||
withDirectives,
|
||||
withKeys,
|
||||
withMemo,
|
||||
withModifiers,
|
||||
withScopeId
|
||||
};
|
||||
//# sourceMappingURL=vue.js.map
|
7
docs/.vitepress/cache/deps/vue.js.map
vendored
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"version": 3,
|
||||
"sources": [],
|
||||
"sourcesContent": [],
|
||||
"mappings": "",
|
||||
"names": []
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
import { defineConfig } from "vitepress";
|
||||
|
||||
// https://vitepress.dev/reference/site-config
|
||||
export default defineConfig({
|
||||
title: "paperai-docs",
|
||||
description: "paperai docs",
|
||||
base: "/paper-ai/",
|
||||
themeConfig: {
|
||||
// https://vitepress.dev/reference/default-theme-config
|
||||
nav: [
|
||||
{ text: "Home", link: "/" },
|
||||
{
|
||||
text: "GitHub",
|
||||
link: "https://github.com/14790897/paper-ai",
|
||||
},
|
||||
],
|
||||
|
||||
sidebar: [
|
||||
{
|
||||
text: "Examples",
|
||||
items: [
|
||||
// { text: "Markdown Examples", link: "/markdown-examples" },
|
||||
// { text: "Runtime API Examples", link: "/api-examples" },
|
||||
{ text: "功能介绍 function", link: "/paperai-function" },
|
||||
{ text: "部署方法 deploy", link: "/paperai-deploy" },
|
||||
{ text: "环境变量 env", link: "/paperai-env" },
|
||||
{ text: "界面展示 show", link: "/paperai-interface" },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
socialLinks: [
|
||||
{ icon: "github", link: "https://github.com/vuejs/vitepress" },
|
||||
],
|
||||
},
|
||||
});
|
|
@ -1,49 +0,0 @@
|
|||
---
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# Runtime API Examples
|
||||
|
||||
This page demonstrates usage of some of the runtime APIs provided by VitePress.
|
||||
|
||||
The main `useData()` API can be used to access site, theme, and page data for the current page. It works in both `.md` and `.vue` files:
|
||||
|
||||
```md
|
||||
<script setup>
|
||||
import { useData } from 'vitepress'
|
||||
|
||||
const { theme, page, frontmatter } = useData()
|
||||
</script>
|
||||
|
||||
## Results
|
||||
|
||||
### Theme Data
|
||||
<pre>{{ theme }}</pre>
|
||||
|
||||
### Page Data
|
||||
<pre>{{ page }}</pre>
|
||||
|
||||
### Page Frontmatter
|
||||
<pre>{{ frontmatter }}</pre>
|
||||
```
|
||||
|
||||
<script setup>
|
||||
import { useData } from 'vitepress'
|
||||
|
||||
const { site, theme, page, frontmatter } = useData()
|
||||
</script>
|
||||
|
||||
## Results
|
||||
|
||||
### Theme Data
|
||||
<pre>{{ theme }}</pre>
|
||||
|
||||
### Page Data
|
||||
<pre>{{ page }}</pre>
|
||||
|
||||
### Page Frontmatter
|
||||
<pre>{{ frontmatter }}</pre>
|
||||
|
||||
## More
|
||||
|
||||
Check out the documentation for the [full list of runtime APIs](https://vitepress.dev/reference/runtime-api#usedata).
|
|
@ -1,30 +0,0 @@
|
|||
---
|
||||
# https://vitepress.dev/reference/default-theme-home-page
|
||||
layout: home
|
||||
|
||||
hero:
|
||||
name: "paperai-docs"
|
||||
text: "paperai docs"
|
||||
tagline: My great project tagline
|
||||
actions:
|
||||
- theme: brand
|
||||
text: paperai-function
|
||||
link: /paperai-function
|
||||
- theme: brand
|
||||
text: paperai-deploy
|
||||
link: /paperai-deploy
|
||||
- theme: brand
|
||||
text: paperai-env
|
||||
link: /paperai-env
|
||||
- theme: alt
|
||||
text: API Examples
|
||||
link: /api-examples
|
||||
|
||||
features:
|
||||
- title: Feature 1 使用真实文献
|
||||
details: 从各种文献网站获取真实文献
|
||||
- title: Feature 2 AI写作
|
||||
details: 内置免费AI模型
|
||||
- title: Feature 3 文献管理
|
||||
details: 将文献有序地排列
|
||||
---
|
|
@ -1,85 +0,0 @@
|
|||
# Markdown Extension Examples
|
||||
|
||||
This page demonstrates some of the built-in markdown extensions provided by VitePress.
|
||||
|
||||
## Syntax Highlighting
|
||||
|
||||
VitePress provides Syntax Highlighting powered by [Shiki](https://github.com/shikijs/shiki), with additional features like line-highlighting:
|
||||
|
||||
**Input**
|
||||
|
||||
````md
|
||||
```js{4}
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
msg: 'Highlighted!'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
````
|
||||
|
||||
**Output**
|
||||
|
||||
```js{4}
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
msg: 'Highlighted!'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Containers
|
||||
|
||||
**Input**
|
||||
|
||||
```md
|
||||
::: info
|
||||
This is an info box.
|
||||
:::
|
||||
|
||||
::: tip
|
||||
This is a tip.
|
||||
:::
|
||||
|
||||
::: warning
|
||||
This is a warning.
|
||||
:::
|
||||
|
||||
::: danger
|
||||
This is a dangerous warning.
|
||||
:::
|
||||
|
||||
::: details
|
||||
This is a details block.
|
||||
:::
|
||||
```
|
||||
|
||||
**Output**
|
||||
|
||||
::: info
|
||||
This is an info box.
|
||||
:::
|
||||
|
||||
::: tip
|
||||
This is a tip.
|
||||
:::
|
||||
|
||||
::: warning
|
||||
This is a warning.
|
||||
:::
|
||||
|
||||
::: danger
|
||||
This is a dangerous warning.
|
||||
:::
|
||||
|
||||
::: details
|
||||
This is a details block.
|
||||
:::
|
||||
|
||||
## More
|
||||
|
||||
Check out the documentation for the [full list of markdown extensions](https://vitepress.dev/guide/markdown).
|
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"devDependencies": {
|
||||
"vitepress": "^1.0.0-rc.42"
|
||||
},
|
||||
"scripts": {
|
||||
"docs:dev": "vitepress dev",
|
||||
"docs:build": "vitepress build",
|
||||
"docs:preview": "vitepress preview"
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
## 镜像运行
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
## vercel 部署
|
||||
|
||||
[![Deploy with Vercel](https://vercel.com/button)](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)
|
||||
|
||||
## 克隆在本地运行
|
||||
|
||||
```bash
|
||||
# 克隆版本库
|
||||
git clone https://github.com/14790897/paper-ai.git
|
||||
|
||||
# 进入项目目录
|
||||
cd paper-ai
|
||||
|
||||
# 安装依赖项
|
||||
npm install
|
||||
|
||||
# 运行项目
|
||||
npm run dev
|
||||
```
|
|
@ -1,7 +0,0 @@
|
|||
|
||||
## 环境变量说明
|
||||
|
||||
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,可以增加请求量
|
|
@ -1,29 +0,0 @@
|
|||
---
|
||||
title: 介绍
|
||||
sidebar: auto
|
||||
---
|
||||
|
||||
## 功能介绍
|
||||
|
||||
- **人工智能书写功能**: 点击 "AI 写作 "进行正常对话互动。人工智能将根据您的输入提供写作建议或回答问题。
|
||||
|
||||
- **寻找文献功能**: 点击 "寻找文献",根据输入的关键词在 Semantic Scholar 或 arxiv 或 PubMed(通过下拉框选择) 中搜索论文。系统将把信息整合到您的论文中。一次搜索两篇,无搜索结果时会显示错误提示。
|
||||
- **文献整理功能**: 每次获得的文献都会被整理到页面下方
|
||||
- **导出到 word**: 页面最下方的导出 word 按钮可以将论文和文献引用直接导出到 word,十分方便
|
||||
- **内置多个免费 AI 模型**:请在右上角设置界面查看
|
||||
|
||||
- **云同步及多篇论文编辑功能**:此功能暂定收费二十元人民币永久开通,请点击左上角列表按钮进行购买,购买前需要注册账号
|
||||
|
||||
![](https://file.liuweiqing.life/2024/02/558e11d675d6a07dedbcfdc5ab8d072f.png)
|
||||
|
||||
## 具体使用方法
|
||||
|
||||
- 左边第一个输入框有两种用法
|
||||
|
||||
1. 点击 AI 写作时会读取输入框的内容进行正常的对话交流
|
||||
2. 点击寻找文献会根据输入的主题词去寻找对应论文(如果输入的是英文关键词就会出现英文文献,中文关键词就是中文文献)
|
||||
|
||||
- 寻找文献按钮旁边是一个选择框,用于选择从哪个网站获取论文,目前可以选择 arxiv,semantic scholar(推荐),pubmed(比较推荐)
|
||||
- 之后是一个选择模型的选择框,用于选择对话的 AI 模型,支持哪些模型由你的 api 决定
|
||||
- 往下看是一个大的输入框,这里用户可以修改内容,AI 生成内容
|
||||
- 最下面是文献管理区域,用于整理搜索到的文献,符合中文引用格式
|
|
@ -1,20 +0,0 @@
|
|||
## 主界面
|
||||
|
||||
![](https://file.liuweiqing.life/2024/02/8efc91ee5f0eff0238b96c8f7c564c05.png)
|
||||
![](https://file.liuweiqing.life/2024/02/56c022ad42fffc76dcf4215f1948cbb6.png)
|
||||
|
||||
## 设置界面
|
||||
|
||||
![](https://file.liuweiqing.life/2024/02/0955a53c01c412bdfd447b74c64585e3.png)
|
||||
|
||||
## 登录/注册界面
|
||||
|
||||
![](https://file.liuweiqing.life/2024/02/2f692952aca0263846e933a8ad9ad22a.png)
|
||||
|
||||
## 商店购买界面
|
||||
|
||||
![](https://file.liuweiqing.life/2024/02/b87d0022933462957ccf52d83c9b77ab.png)
|
||||
|
||||
## VIP 功能(云端同步,多篇文章编辑)
|
||||
|
||||
![](https://file.liuweiqing.life/2024/02/fd3b4fd44450a2276466b01b57b11c95.png)
|
|
@ -69,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.ico).*)",
|
||||
"/((?!_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).*)",
|
||||
],
|
||||
};
|
||||
|
|
|
@ -6,57 +6,18 @@ const nextConfig = {
|
|||
};
|
||||
|
||||
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(
|
||||
module.exports,
|
||||
{
|
||||
// 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: "javascript-nextjs",
|
||||
},
|
||||
{
|
||||
// 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,
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
// Injected content via Sentry wizard below
|
||||
|
||||
const { withSentryConfig } = require("@sentry/nextjs");
|
||||
|
||||
module.exports = withSentryConfig(
|
||||
module.exports,
|
||||
enhancedConfig,
|
||||
{
|
||||
// For all available options, see:
|
||||
// https://github.com/getsentry/sentry-webpack-plugin#options
|
||||
|
|
1173
package-lock.json
generated
16
package.json
|
@ -16,9 +16,11 @@
|
|||
"@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",
|
||||
|
@ -30,7 +32,8 @@
|
|||
"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",
|
||||
|
@ -38,7 +41,7 @@
|
|||
"react-cookie": "^7.0.2",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-i18next": "^14.0.5",
|
||||
"react-quill": "^2.0.0",
|
||||
"react-icons": "^5.0.1",
|
||||
"react-redux": "^9.1.0",
|
||||
"react-toastify": "^10.0.4",
|
||||
"react-transition-group": "^4.4.5",
|
||||
|
@ -46,16 +49,13 @@
|
|||
"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.4",
|
||||
"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",
|
||||
|
@ -65,5 +65,5 @@
|
|||
"@types/redux-logger": "^3.0.12",
|
||||
"encoding": "^0.1.13"
|
||||
},
|
||||
"version": "1.6.0"
|
||||
"version": "1.9.0"
|
||||
}
|
||||
|
|
BIN
public/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 42 KiB |
BIN
public/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 242 KiB |
BIN
public/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 38 KiB |
BIN
public/favicon-16x16.png
Normal file
After Width: | Height: | Size: 766 B |
BIN
public/favicon-32x32.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
52
public/manifest.json
Normal 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
|
@ -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
|
@ -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
|
@ -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"}
|
|
@ -3,28 +3,35 @@
|
|||
// 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",
|
||||
|
||||
Sentry.init({
|
||||
dsn: "https://523c4056ba48d012c62a377dfc49f647@o4506728662564864.ingest.sentry.io/4506728672264192",
|
||||
// Adjust this value in production, or use tracesSampler for greater control
|
||||
tracesSampleRate: 1,
|
||||
|
||||
// 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,
|
||||
|
||||
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||
debug: false,
|
||||
replaysOnErrorSampleRate: 1.0,
|
||||
|
||||
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,
|
||||
|
||||
// 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,
|
||||
}),
|
||||
],
|
||||
});
|
||||
// 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,
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
|
|
@ -4,13 +4,14 @@
|
|||
// 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",
|
||||
|
||||
Sentry.init({
|
||||
dsn: "https://523c4056ba48d012c62a377dfc49f647@o4506728662564864.ingest.sentry.io/4506728672264192",
|
||||
// Adjust this value in production, or use tracesSampler for greater control
|
||||
tracesSampleRate: 1,
|
||||
|
||||
// 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,
|
||||
});
|
||||
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||
debug: false,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -3,13 +3,14 @@
|
|||
// 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",
|
||||
|
||||
Sentry.init({
|
||||
dsn: "https://523c4056ba48d012c62a377dfc49f647@o4506728662564864.ingest.sentry.io/4506728672264192",
|
||||
// Adjust this value in production, or use tracesSampler for greater control
|
||||
tracesSampleRate: 1,
|
||||
|
||||
// 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,
|
||||
});
|
||||
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||
debug: false,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -18,6 +18,15 @@ module.exports = {
|
|||
},
|
||||
gold: "#FFD700",
|
||||
},
|
||||
animation: {
|
||||
spin: "spin 1s linear infinite",
|
||||
},
|
||||
keyframes: {
|
||||
spin: {
|
||||
"0%": { transform: "rotate(0deg)" },
|
||||
"100%": { transform: "rotate(360deg)" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
|
|
154
utils/others/aiutils.ts
Normal file
|
@ -0,0 +1,154 @@
|
|||
// // Path: utils/others/aiutils.ts
|
||||
// import { sendMessageToOpenAI } from "@/components/chatAI";
|
||||
// //判断返回的文献是否跟用户输入的主题相关
|
||||
// export async function evaluateTopicMatch(
|
||||
// userMessage: any[],
|
||||
// apiKey: string,
|
||||
// upsreamUrl: string,
|
||||
// selectedModel: string,
|
||||
// topic: string
|
||||
// ): Promise<{ relevantPapers: string[]; nonRelevantPapers: string[] }> {
|
||||
// const prompt = "请判断文献是否跟用户输入的主题相关,只需要返回true或者false";
|
||||
// let relevantPapers: string[] = []; // 存储相关论文的数组
|
||||
// let nonRelevantPapers: string[] = []; // 存储不相关论文的数组
|
||||
|
||||
// for (const paper of userMessage) {
|
||||
// if (relevantPapers.length >= 2) {
|
||||
// console.log("已找到两篇相关文献,停止处理剩余文献。");
|
||||
// break; // 如果已经有两篇相关文献,则停止处理
|
||||
// }
|
||||
// // 检查是否存在 abstract,如果不存在,直接将论文添加到 nonRelevantPapers
|
||||
// if (!paper.abstract) {
|
||||
// nonRelevantPapers.push(paper);
|
||||
// console.log(
|
||||
// `Paper titled "${paper.title}" has no abstract and was added to non-relevant papers.`
|
||||
// );
|
||||
// continue; // 跳过当前迭代,继续下一个论文
|
||||
// }
|
||||
// const input = `user's topic:${topic}, \n paper's title: ${paper.title}, \n paper's abstract: ${paper.abstract}`;
|
||||
// const isRelevantResult = await sendMessageToOpenAI(
|
||||
// input,
|
||||
// null,
|
||||
// selectedModel!,
|
||||
// apiKey,
|
||||
// upsreamUrl,
|
||||
// prompt,
|
||||
// null,
|
||||
// false
|
||||
// );
|
||||
// console.log("isRelevantResult", isRelevantResult);
|
||||
// // 尝试解析 JSON 结果,如果无法解析则直接使用结果字符串
|
||||
// let isRelevant;
|
||||
// try {
|
||||
// const parsedResult = JSON.parse(isRelevantResult);
|
||||
// isRelevant =
|
||||
// parsedResult === true || parsedResult.toLowerCase() === "true";
|
||||
// } catch {
|
||||
// isRelevant =
|
||||
// isRelevantResult.includes("true") || isRelevantResult.includes("True");
|
||||
// }
|
||||
|
||||
// if (isRelevant) {
|
||||
// relevantPapers.push(paper); // 如果论文相关,则添加到数组中
|
||||
// } else {
|
||||
// nonRelevantPapers.push(paper); // 如果论文不相关,则添加到不相关论文数组中
|
||||
// }
|
||||
// }
|
||||
// console.log(
|
||||
// `这次有${nonRelevantPapers.length}篇文献没有通过相关性检查`,
|
||||
// nonRelevantPapers
|
||||
// );
|
||||
// //如果相关文献大于两片则缩减到两篇
|
||||
// // if (relevantPapers.length > 2) {
|
||||
// // relevantPapers = relevantPapers.slice(0, 2);
|
||||
// // console.log("文献太多了,只取前两篇");
|
||||
// // }
|
||||
// return { relevantPapers, nonRelevantPapers };
|
||||
// }
|
||||
|
||||
// Path: utils/others/aiutils.ts
|
||||
import { sendMessageToOpenAI } from "@/components/chatAI";
|
||||
//判断返回的文献是否跟用户输入的主题相关
|
||||
export async function evaluateTopicMatch(
|
||||
userMessage: any[],
|
||||
apiKey: string,
|
||||
upsreamUrl: string,
|
||||
selectedModel: string,
|
||||
topic: string,
|
||||
signal: AbortSignal
|
||||
): Promise<{ relevantPapers: string[]; nonRelevantPapers: string[] }> {
|
||||
const prompt =
|
||||
"请判断文献是否跟用户输入的主题相关,只需要返回true或false的数组";
|
||||
let relevantPapers: string[] = []; // 存储相关论文的数组
|
||||
let nonRelevantPapers: string[] = []; // 存储不相关论文的数组
|
||||
|
||||
// 修改循环逻辑,每次处理两篇文献
|
||||
for (let i = 0; i < userMessage.length; i += 2) {
|
||||
// 检查是否已有足够的相关论文
|
||||
if (relevantPapers.length >= 2) {
|
||||
console.log("已找到两篇相关文献,停止处理剩余文献。");
|
||||
break;
|
||||
}
|
||||
|
||||
let inputs = [];
|
||||
let papersToEvaluate = userMessage.slice(i, i + 2); // 获取当前迭代的两篇文献
|
||||
|
||||
for (const paper of papersToEvaluate) {
|
||||
// 检查是否存在 abstract,如果不存在,直接将论文添加到 nonRelevantPapers
|
||||
if (!paper.abstract) {
|
||||
nonRelevantPapers.push(paper);
|
||||
console.log(
|
||||
`Paper titled "${paper.title}" has no abstract and was added to non-relevant papers.`
|
||||
);
|
||||
continue; // 跳过当前论文,继续下一个论文
|
||||
}
|
||||
// 准备输入数据
|
||||
const input = `user's topic: ${topic}, \n paper's title: ${paper.title}, \n paper's abstract: ${paper.abstract}`;
|
||||
inputs.push(input);
|
||||
}
|
||||
|
||||
// 如果有需要评估的文献,则发送请求
|
||||
if (inputs.length > 0) {
|
||||
const combinedInput = inputs.join("\n\n");
|
||||
const isRelevantResults = await sendMessageToOpenAI(
|
||||
combinedInput,
|
||||
null,
|
||||
selectedModel!,
|
||||
apiKey,
|
||||
upsreamUrl,
|
||||
prompt,
|
||||
null,
|
||||
false,
|
||||
signal
|
||||
);
|
||||
console.log("isrelevantResults in 相关性检查", isRelevantResults);
|
||||
// 处理每篇文献的相关性结果
|
||||
papersToEvaluate.forEach((paper, index) => {
|
||||
let isRelevant;
|
||||
try {
|
||||
const parsedResults = JSON.parse(isRelevantResults); // 将字符串解析成数组
|
||||
isRelevant =
|
||||
parsedResults[index] === true ||
|
||||
parsedResults[index].toString().toLowerCase() === "true";
|
||||
} catch {
|
||||
console.log("Error parsing isRelevantResults or accessing index");
|
||||
}
|
||||
|
||||
if (isRelevant) {
|
||||
relevantPapers.push(paper);
|
||||
console.log(`Paper titled "${paper.title}" is relevant.`);
|
||||
} else {
|
||||
nonRelevantPapers.push(paper);
|
||||
console.log(`Paper titled "${paper.title}" is not relevant.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
`这次有${nonRelevantPapers.length}篇文献没有通过相关性检查`,
|
||||
nonRelevantPapers
|
||||
);
|
||||
|
||||
return { relevantPapers, nonRelevantPapers };
|
||||
}
|
|
@ -323,14 +323,25 @@ export function getFullReference(reference: Reference) {
|
|||
fullReference += formatReference(reference);
|
||||
return fullReference;
|
||||
}
|
||||
export function getAllFullReferences(references: Reference[]) {
|
||||
export function getAllFullReferences(references: Reference[], style: string) {
|
||||
return references
|
||||
.map((reference, index) => {
|
||||
return `[${index + 1}] ${getFullReference(reference)}`;
|
||||
return `[${index + 1}] ${renderCitation(reference, style)}`;
|
||||
})
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
export function renderCitation(reference: any, style: string) {
|
||||
// 检查当前的引用风格
|
||||
if (style === "custom-chinese") {
|
||||
// 如果是“custom-chinese”,则调用 getFullReference 来渲染引用
|
||||
return getFullReference(reference);
|
||||
} else {
|
||||
// 否则,返回引用对象中对应风格的引用文本
|
||||
return reference[style];
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
getTextBeforeCursor,
|
||||
updateBracketNumbersInDelta,
|
||||
|
|
47
utils/supabase/serverutils.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
import { NextResponse } from "next/server";
|
||||
|
||||
import { SupabaseClient } from "@supabase/supabase-js";
|
||||
export async function setVip(
|
||||
supabaseAdmin: SupabaseClient,
|
||||
userId: string,
|
||||
isVip = true,
|
||||
source = "Linuxdo",
|
||||
startDate = new Date(),
|
||||
endDate = new Date()
|
||||
) {
|
||||
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,
|
||||
source: source,
|
||||
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 状态已更新:" });
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
|
@ -178,20 +178,36 @@ export async function fetchUserVipStatus(userId: string) {
|
|||
|
||||
//profiles表 插入用户信息
|
||||
export async function insertUserProfile(data: any, supabase: SupabaseClient) {
|
||||
const user = data?.user;
|
||||
let user;
|
||||
if (data.user) {
|
||||
user = data.user;
|
||||
} else {
|
||||
user = data;
|
||||
}
|
||||
|
||||
if (user) {
|
||||
// console.log("user in insertUserProfile:", user);
|
||||
const currentTime = new Date().toISOString(); // 生成ISO格式的时间字符串
|
||||
|
||||
const { data, error: profileError } = await supabase
|
||||
.from("profiles")
|
||||
.insert([{ id: user.id, email: user.email }]);
|
||||
.upsert([
|
||||
{
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
created_at: currentTime, // 添加创建时间
|
||||
},
|
||||
]);
|
||||
|
||||
if (profileError) {
|
||||
console.error("Failed to create user profile:", profileError);
|
||||
Sentry.captureException(profileError);
|
||||
}
|
||||
//sentry
|
||||
|
||||
Sentry.setUser({
|
||||
email: user.email,
|
||||
id: user.id,
|
||||
ip_address: "{{auto}}}",
|
||||
ip_address: "{{auto}}",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|