Merge pull request #13 from 14790897/supa

Supa and Lemon
This commit is contained in:
liuweiqing 2024-02-09 23:03:02 +08:00 committed by GitHub
commit 729de74653
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 1523 additions and 201 deletions

1
.gitignore vendored
View File

@ -36,3 +36,4 @@ yarn-error.log*
next-env.d.ts
pass
.env

View File

@ -0,0 +1,92 @@
// app/api/payment/webhooks/route.ts
import { headers } from "next/headers";
import { Buffer } from "buffer";
import crypto from "crypto";
import rawBody from "raw-body";
import { Readable } from "stream";
import { NextResponse } from "next/server";
import { createClient } from "@/utils/supabase/server";
import { cookies } from "next/headers";
import { SupabaseClient } from "@supabase/supabase-js";
export async function POST(request: Request) {
const cookieStore = cookies();
const supabaseAdmin = createClient(cookieStore);
console.log("webhook");
const body = await rawBody(Readable.from(Buffer.from(await request.text())));
const headersList = headers();
const payload = JSON.parse(body.toString());
const sigString = headersList.get("x-signature");
if (!sigString) {
console.error(`Signature header not found`);
return NextResponse.json(
{ message: "Signature header not found" },
{ status: 401 }
);
}
const secret = process.env.LEMONS_SQUEEZY_SIGNATURE_SECRET as string;
const hmac = crypto.createHmac("sha256", secret);
const digest = Buffer.from(hmac.update(body).digest("hex"), "utf8");
const signature = Buffer.from(
Array.isArray(sigString) ? sigString.join("") : sigString || "",
"utf8"
);
// 校验签名
if (!crypto.timingSafeEqual(digest, signature)) {
return NextResponse.json({ message: "Invalid signature" }, { status: 403 });
}
const userEmail = (payload.attributes && payload.attributes.user_email) || "";
// 检查custom里的参数
if (!userEmail)
return NextResponse.json(
{ message: "No userEmail provided" },
{ status: 403 }
);
return await setVip(supabaseAdmin, userEmail);
}
async function getUserId(supabaseAdmin: SupabaseClient, email: string) {
const { data, error } = await supabaseAdmin
.from("auth.users")
.select("id")
.eq("email", email)
.single();
if (error) {
console.error("查询用户 ID 失败:", error);
return null;
}
return data.id;
}
async function setVip(
supabaseAdmin: SupabaseClient,
email: string,
isVip = true,
startDate = new Date(),
endDate = new Date()
) {
const userId = await getUserId(supabaseAdmin, email);
if (!userId)
return NextResponse.json({ message: "No user found" }, { status: 403 });
const { data, error } = await supabaseAdmin.from("vip_statuses").upsert(
{
user_id: userId,
is_vip: isVip,
start_date: startDate,
end_date: endDate,
},
{ onConflict: "user_id" }
);
if (error) {
console.error("设置 VIP 失败:", error);
return NextResponse.json(
{ message: "Failed to set VIP 设置 VIP 状态失败" },
{ status: 403 }
);
}
return NextResponse.json({ message: "Success VIP 状态已更新:" });
}

View File

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

View File

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

View File

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

View File

View File

@ -76,3 +76,36 @@
.vip-icon {
animation: flash 1s linear infinite;
}
/* 动画的基本样式 */
.slide-enter {
opacity: 0;
transform: translateY(100%); /* 从底部滑入 */
}
.slide-enter-active {
opacity: 1;
transform: translateY(0);
transition: opacity 300ms ease-out, transform 300ms ease-out;
}
.slide-exit {
opacity: 1;
}
.slide-exit-active {
opacity: 0;
transform: translateY(100%); /* 向底部滑出 */
transition: opacity 300ms ease-in, transform 300ms ease-in;
}
.paper-management-container {
position: fixed; /* 或者使用 `absolute` 根据需要 */
top: 50%; /* 调整到视口的垂直中心 */
left: 50%; /* 调整到视口的水平中心 */
background-color: rgba(255, 255, 255, 0.5);
transform: translate(
-50%,
-50%
); /* 从中心点向上和向左偏移自身的50%,确保组件居中 */
z-index: 1000; /* 确保悬浮层在其他内容之上 */
/* 可以添加其他样式来美化组件,如背景色、阴影等 */
}

View File

@ -1,5 +1,5 @@
import DeployButton from "../components/DeployButton";
import AuthButton from "../components/AuthButton";
import PaperListButtonWrapper from "@/components/PaperListButtonWrapper";
import AuthButton from "@/components/AuthButton";
import { createClient } from "@/utils/supabase/server";
import ConnectSupabaseSteps from "@/components/ConnectSupabaseSteps";
import SignUpUserSteps from "@/components/SignUpUserSteps";
@ -9,11 +9,10 @@ import QuillWrapper from "./QuillWrapper";
// import TinyEditor from "../components/TinyEditor";
// import SEditor from "../components/SlateEditor";
import SettingsLink from "@/components/SettingsLink";
import { ErrorBoundary } from "next/dist/client/components/error-boundary";
// import React, { useState, useEffect, useRef } from "react";
import PaperManagementWrapper from "@/components/PaperManagementWrapper";
// import Error from "@/app/global-error";
export default async function Index() {
export default function Index() {
const cookieStore = cookies();
const canInitSupabaseClient = () => {
@ -30,18 +29,18 @@ export default async function Index() {
const isSupabaseConnected = canInitSupabaseClient();
return (
<div className="flex-1 w-full flex flex-col gap-20 items-center">
<div className="flex-1 w-full flex flex-col gap-10 items-center">
<nav className="w-full flex justify-center border-b border-b-foreground/10 h-16">
<div className="w-full max-w-4xl flex justify-between items-center p-3 text-sm">
<DeployButton />
{/* <DeployButton /> */}
{/* 用来表示是否显示论文列表页 */}
<PaperListButtonWrapper />
{isSupabaseConnected && <AuthButton />}
<SettingsLink />
</div>
</nav>
{/* <ErrorBoundary fallback={<Error />}> */}
<PaperManagementWrapper />
<QuillWrapper />
{/* </ErrorBoundary> */}
<footer className="w-full border-t border-t-foreground/10 p-8 flex justify-center text-center text-xs">
<p>
<a

View File

@ -3,6 +3,7 @@ import { useDispatch, TypedUseSelectorHook, useSelector } from "react-redux";
import { persistReducer } from "redux-persist";
// import storage from "redux-persist/lib/storage";
import { authReducer } from "./slices/authSlice";
import { stateReducer } from "./slices/stateSlice";
import storage from "./customStorage";
import logger from "redux-logger";
@ -18,14 +19,26 @@ const authPersistConfig = {
],
};
const statePersistConfig = {
key: "state1",
storage: storage,
whitelist: [
"showPaperManagement",
"paperNumberRedux",
"contentUpdatedFromNetwork",
"isVip",
],
};
const rootReducer = combineReducers({
auth: persistReducer(authPersistConfig, authReducer),
state: persistReducer(statePersistConfig, stateReducer),
});
export const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({ serializableCheck: false }).concat(logger),
getDefaultMiddleware({ serializableCheck: false }).concat(logger), //.concat(logger)
});
export type RootState = ReturnType<typeof store.getState>;

View File

@ -6,20 +6,33 @@ export interface APIState {
editorContent: string;
upsreamUrl: string;
systemPrompt: string;
showPaperManagement: boolean;
}
const initialState: APIState = {
apiKey: "sess-L6DwIB7N859iQLWfNBTaPsmkErqZrjoXVk6m7BmA",
apiKey: "sk-aiHrrRLYUUelHstX69E9484509254dBf92061d6744FfFaD1", //sk-ffe19ebe9fa44d00884330ff1c18cf82
referencesRedux: [],
editorContent: "",
upsreamUrl: "https://api.liuweiqing.top", //https://api.openai.com https://one.caifree.com https://chatserver.3211000.xyz
systemPrompt: "",
upsreamUrl: "https://one.caifree.com", //https://api.openai.com https://one.caifree.com https://chatserver.3211000.xyz https://api.deepseek.com
systemPrompt: `作为论文写作助手,您的主要任务是根据用户提供的研究主题和上下文,以及相关的研究论文,来撰写和完善学术论文。在撰写过程中,请注意以下要点:
1.使
2.使 [1]***[1]*
3.
4.
5.使,
6.
...[1],...[2]`,
showPaperManagement: false,
};
export const authSlice = createSlice({
name: "auth",
initialState,
reducers: {
setShowPaperManagement: (state, action: PayloadAction<boolean>) => {
state.showPaperManagement = action.payload;
},
setApiKey: (state, action: PayloadAction<string>) => {
state.apiKey = action.payload;
},
@ -52,6 +65,9 @@ export const authSlice = createSlice({
clearReferencesRedux: (state) => {
state.referencesRedux = [];
},
setReferencesRedux: (state, action: PayloadAction<Reference[]>) => {
state.referencesRedux = action.payload;
},
swapReferencesRedux: (
state,
action: PayloadAction<{ indexA: number; indexB: number }>
@ -77,6 +93,7 @@ export const authSlice = createSlice({
// Action creators are generated for each case reducer function
export const {
setShowPaperManagement,
setApiKey,
setUpsreamUrl,
addReferenceRedux,
@ -84,6 +101,7 @@ export const {
removeReferenceRedux,
clearReferencesRedux,
setEditorContent,
setReferencesRedux,
setSystemPrompt,
swapReferencesRedux,
} = authSlice.actions;

View File

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

View File

@ -1,7 +1,10 @@
// "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";
export default async function AuthButton() {
const cookieStore = cookies();
@ -17,15 +20,16 @@ export default async function AuthButton() {
const cookieStore = cookies();
const supabase = createClient(cookieStore);
await supabase.auth.signOut();
return redirect("/login");
};
return user ? (
<div className="flex items-center gap-4">
Hey, {user.email}!
<div className="vip-icon bg-yellow-400 text-white p-2 rounded-full shadow-lg animate-pulse">
{/* <div className="vip-icon bg-yellow-400 text-white p-2 rounded-full shadow-lg animate-pulse">
VIP
</div>
</div> */}
<form action={signOut}>
<button className="py-2 px-4 rounded-md no-underline bg-btn-background hover:bg-btn-background-hover">
Logout

View File

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

View File

@ -53,18 +53,15 @@ async function getArxivPapers(
// 你可以在这里处理数据
result = extractArxivData(result);
return result;
} catch (error) {
if (error.response) {
// 请求已发送,但服务器响应的状态码不在 2xx 范围内
console.error("Error fetching data: ", error.response.data);
} else if (error.request) {
// 请求已发送,但没有收到响应
console.error("No response received: ", error.request);
} else {
// 发送请求时出现错误
console.error("Error setting up the request: ", error.message);
}
return null;
} catch (error: any) {
throw new Error(
`Error fetching data from Arxiv API:${JSON.stringify(
error.response,
null,
2
)}`
);
// return null;
}
}

View File

@ -53,7 +53,7 @@ async function getPubMedPaperDetails(idList: IDList) {
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
throw new Error(`${response.text()}`);
}
const data = await response.text(); // 获取响应文本
@ -152,9 +152,9 @@ async function fetchPubMedData(query: string, year: number, limit: number) {
console.log("fetchPubMedData", paperDetails); // 处理或显示文章详情
return paperDetails;
}
} catch {
console.error("未搜索到文献");
throw new Error("未搜索到文献");
} catch (error) {
//这里无法起作用因为pubmed不会返回400系错误
throw new Error(`未搜索到文献: ${error}`);
}
}

View File

@ -1,5 +1,5 @@
import axios from "axios";
import {getRandomOffset} from "@/utils/others/quillutils"
import { getRandomOffset } from "@/utils/others/quillutils";
interface Author {
authorId: string;
name: string;
@ -22,7 +22,7 @@ async function getSemanticPapers(query: string, year: string, limit = 2) {
const url = `https://api.semanticscholar.org/graph/v1/paper/search`;
const response = await axios.get(url, {
headers: {
'x-api-key': process.env.NEXT_PUBLIC_SEMANTIC_API_KEY,
"x-api-key": process.env.NEXT_PUBLIC_SEMANTIC_API_KEY,
},
params: {
query: query,
@ -33,7 +33,7 @@ async function getSemanticPapers(query: string, year: string, limit = 2) {
},
});
// 提取并处理论文数据
const papers = response.data.data.map((paper:Paper) => {
const papers = response.data.data.map((paper: Paper) => {
// 提取每篇论文的作者名字
const authorNames = paper.authors.map((author) => author.name);
@ -43,13 +43,19 @@ async function getSemanticPapers(query: string, year: string, limit = 2) {
};
});
return papers;
} catch (error) {
console.error("Error fetching data from Semantic Scholar API:", error);
return null; // 或根据需要处理错误
} catch (error: any) {
// console.error("Error fetching data from Semantic Scholar API:", error);
throw new Error(
`Error fetching data from Semantic Scholar API:${JSON.stringify(
error.response,
null,
2
)}`
);
// return null;
}
}
// 调用函数示例
// fetchSemanticPapers("covid", 50, 2, "2015-2023").then((data) => {
// console.log(data);

View File

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

View File

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

View File

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

View File

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

View File

@ -8,14 +8,18 @@ interface SweetAlertComponentProps {
removeReferenceUpdateIndex: (index: number, rmPg: boolean) => void;
}
const ParagraphDeleteButton: React.FC<SweetAlertComponentProps> = ({
const ParagraphDeleteButton: React.FC<any> = ({
index,
removeReferenceUpdateIndex,
isRemovePaper = false,
title = "需要同时删除与文献相关的整个段落吗?",
text = "根据周围的换行符来判断是否是同一个段落",
}) => {
//这里传递函数的时候应该把参数先提前弄好 2.7
const showAlert = async () => {
const result = await Swal.fire({
title: "需要同时删除与文献相关的整个段落吗?",
text: "根据周围的换行符来判断是否是同一个段落",
title: title,
text: text,
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#3085d6",
@ -23,10 +27,14 @@ const ParagraphDeleteButton: React.FC<SweetAlertComponentProps> = ({
confirmButtonText: "Yes, delete it!",
});
if (result.isConfirmed) {
if (isRemovePaper) {
removeReferenceUpdateIndex(index, true);
} else {
removeReferenceUpdateIndex();
}
// Swal.fire("Deleted!", "Your file has been deleted.", "success");
} else {
removeReferenceUpdateIndex(index, false);
if (!isRemovePaper) removeReferenceUpdateIndex(index, false);
// Swal.fire("Cancelled", "Your imaginary file is safe :)", "error");
}
};

View File

@ -27,8 +27,18 @@ 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";
const toolbarOptions = [
["bold", "italic", "underline", "strike"], // 加粗、斜体、下划线和删除线
@ -55,6 +65,12 @@ const QEditor = () => {
const apiKey = useAppSelector((state: any) => state.auth.apiKey);
const upsreamUrl = useAppSelector((state: any) => state.auth.upsreamUrl);
const [quill, setQuill] = useState<Quill | null>(null);
const contentUpdatedFromNetwork = useAppSelector(
(state) => state.state.contentUpdatedFromNetwork
);
//vip状态
const isVip = useAppSelector((state) => state.state.isVip);
//询问ai用户输入
const [userInput, setUserInput] = useState("robot");
//quill编辑器鼠标位置
@ -65,20 +81,24 @@ const QEditor = () => {
const editor = useRef<Quill | null>(null);
// 选择论文来源
const [selectedSource, setSelectedSource] = useLocalStorage(
"semanticScholar",
"学术引擎",
"semanticScholar"
); // 默认选项
//选择语言模型
const [selectedModel, setSelectedModel] = useLocalStorage(
"gpt3.5",
"deepseek-chat"
"gpt语言模型",
"gpt-4"
); // 默认选项
//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
);
//supabase
const supabase = createClient();
useEffect(() => {
if (!isMounted.current) {
editor.current = new Quill("#editor", {
@ -114,7 +134,6 @@ const QEditor = () => {
const range = editor.current!.getSelection();
if (range && range.length === 0 && editor.current) {
const [leaf, offset] = editor.current.getLeaf(range.index);
// console.log("leaf", leaf);
if (leaf.text) {
const textWithoutSpaces = leaf.text.replace(/\s+/g, ""); // 去掉所有空格
if (/^\[\d+\]$/.test(textWithoutSpaces)) {
@ -136,28 +155,62 @@ const QEditor = () => {
}
}, []);
// 监听editorContent变化(redux的变量)并使用Quill API更新内容
useEffect(() => {
if (editor.current) {
if (editorContent) {
if (contentUpdatedFromNetwork) {
// 清空当前内容
editor.current.setContents([]);
// 插入新内容
editor.current.clipboard.dangerouslyPasteHTML(editorContent);
// 重置标志
dispatch(setContentUpdatedFromNetwork(false));
} else {
console.log("No content updated from network in useEffect.");
}
} else {
console.log("No editorContent to update in useEffect.");
}
} else {
console.log("No editor.current to update in useEffect.");
}
}, [editorContent, contentUpdatedFromNetwork]);
const handleTextChange = debounce(async function (delta, oldDelta, source) {
if (source === "user") {
// 获取编辑器内容
const content = quill!.root.innerHTML; // 或 quill.getText(),或 quill.getContents()
dispatch(setEditorContent(content)); // 更新 Redux store
//在云端同步supabase
// console.log("paperNumberRedux in quill", paperNumberRedux);
if (isVip) {
const data = await submitPaper(
supabase,
content,
undefined,
paperNumberRedux
);
}
setTimeout(() => {
convertToSuperscript(quill!);
}, 0); // 延迟 0 毫秒,即将函数放入事件队列的下一个循环中执行,不然就会因为在改变文字触发整个函数时修改文本内容造成无法找到光标位置
}
}, 1000); // 这里的 1000 是防抖延迟时间,单位为毫秒
useEffect(() => {
if (quill) {
// 设置监听器以处理内容变化
quill.on("text-change", function (delta, oldDelta, source) {
if (source === "user") {
// 获取编辑器内容
const content = quill.root.innerHTML; // 或 quill.getText(),或 quill.getContents()
// 保存到 localStorage
// localStorage.setItem("quillContent", content);
dispatch(setEditorContent(content)); // 更新 Redux store
setTimeout(() => {
convertToSuperscript(quill);
}, 0); // 延迟 0 毫秒,即将函数放入事件队列的下一个循环中执行,不然就会因为在改变文字触发整个函数时修改文本内容造成无法找到光标位置
quill.on("text-change", handleTextChange);
// 清理函数
return () => {
quill.off("text-change", handleTextChange);
};
}
});
}
}, [quill, dispatch]);
}, [quill, dispatch, paperNumberRedux]);
// 处理用户输入变化
const handleInputChange = (event) => {
const handleInputChange = (event: any) => {
setUserInput(event.target.value);
};
@ -237,6 +290,9 @@ const QEditor = () => {
.join("");
} else if (selectedSource === "pubmed") {
rawData = await fetchPubMedData(topic, 2020, 2);
if (!rawData) {
throw new Error("未搜索到文献 from PubMed.");
}
newReferences = rawData.map((entry) => ({
id: entry.id, // 文章的 PubMed ID
title: entry.title, // 文章的标题
@ -257,6 +313,7 @@ const QEditor = () => {
})
.join("");
}
//在对应的位置添加文献
const nearestNumber = getNumberBeforeCursor(quill);
dispatch(
addReferencesRedux({
@ -287,10 +344,19 @@ const QEditor = () => {
// 重新获取更新后的内容并更新 Redux store
const updatedContent = quill.root.innerHTML;
dispatch(setEditorContent(updatedContent));
if (isVip) {
//在云端同步supabase
const data = await submitPaper(
supabase,
updatedContent,
references,
paperNumberRedux
);
}
} catch (error) {
console.error("Error fetching data:", error);
// console.error("Error fetching data:", error);
// 在处理错误后,再次抛出这个错误
throw error;
throw new Error(`Paper2AI出现错误: ${error}`);
}
}
@ -310,12 +376,6 @@ const QEditor = () => {
>
AI Write
</button>
{/* <button
onClick={() => insertPapers(userInput)}
className="bg-indigo-500 hover:bg-indigo-700 text-black font-bold py-2 px-4 rounded"
>
Insert Papers
</button> */}
<button
onClick={() => paper2AI(userInput)}
className="bg-yellow-500 hover:bg-yellow-600 text-white font-bold py-2 px-4 mr-2 rounded"
@ -337,13 +397,11 @@ const QEditor = () => {
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">gpt3.5</option>
<option value="gpt-4">gpt4</option>
<option value="gpt-3.5-turbo">gpt-3.5-turbo</option>
<option value="gpt-4">gpt-4</option>
<option value="deepseek-chat">deepseek-chat</option>
{/* 其他来源网站 */}
</select>
{/* 用户输入自己的API key */}
<button
onClick={() => formatTextInEditor(quill)} // 假设 updateIndex 是处理更新操作的函数
className="bg-gray-300 hover:bg-gray-400 text-black font-bold py-2 px-4 rounded"

View File

@ -18,7 +18,9 @@ import {
clearReferencesRedux,
swapReferencesRedux,
} from "@/app/store/slices/authSlice";
//supabase
import { submitPaper } from "@/utils/supabase/supabaseutils";
import { createClient } from "@/utils/supabase/client";
type ReferenceListProps = {
editor: any;
};
@ -33,6 +35,11 @@ function ReferenceList({ editor }: ReferenceListProps) {
//redux
const dispatch = useAppDispatch();
const references = useAppSelector((state) => state.auth.referencesRedux);
const paperNumberRedux = useAppSelector(
(state) => state.state.paperNumberRedux
);
//supabase
const supabase = createClient();
function moveReferenceUp(index: number) {
console.log("index", index);
@ -70,11 +77,16 @@ function ReferenceList({ editor }: ReferenceListProps) {
const handleClearReferences = () => {
dispatch(clearReferencesRedux());
};
//监听references如果发生变化就提交到服务器
React.useEffect(() => {
submitPaper(supabase, undefined, references, paperNumberRedux);
}, [references]);
return (
<div className="container mx-auto p-4">
{/* 表单区域 */}
<form
onSubmit={(e) => {
onSubmit={async (e) => {
e.preventDefault();
handleAddReference({
title: newTitle,
@ -89,6 +101,7 @@ function ReferenceList({ editor }: ReferenceListProps) {
setNewYear("");
setNewPublisher("");
setNewUrl("");
// submitPaper(supabase, undefined, references, paperNumberRedux);
}}
className="mb-6"
>
@ -210,6 +223,7 @@ function ReferenceList({ editor }: ReferenceListProps) {
</button>
<ParagraphDeleteButton
index={index}
isRemovePaper={true}
removeReferenceUpdateIndex={removeReferenceUpdateIndex}
></ParagraphDeleteButton>
</li>

View File

@ -10,6 +10,31 @@ import {
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faArrowLeft } from "@fortawesome/free-solid-svg-icons";
import Link from "next/link";
import { useLocalStorage } from "react-use";
// 在 Settings.tsx 或一个单独的配置文件中
const CONFIG_OPTIONS = [
{
name: "cocopilot-gpt4apiKey在前面手动加上ghu",
apiKey: "_pXVxLPBzcvCjSvG0Mv4K7G9ffw3xsM2ZKolZ",
upstreamUrl: "https://proxy.cocopilot.org",
},
{
name: "deepseek-chat(需要手动修改模型为这个)",
apiKey: "sk-ffe19ebe9fa44d00884330ff1c18cf82",
upstreamUrl: "https://api.deepseek.com",
},
{
name: "caifree(推荐)",
apiKey: "sk-aiHrrRLYUUelHstX69E9484509254dBf92061d6744FfFaD1",
upstreamUrl: "https://one.caifree.com",
},
{
name: "自定义",
apiKey: "",
upstreamUrl: "",
},
];
const Settings = () => {
//redux
@ -17,7 +42,11 @@ const Settings = () => {
const apiKey = useAppSelector((state) => state.auth.apiKey);
const upstreamUrl = useAppSelector((state) => state.auth.upsreamUrl);
const systemPrompt = useAppSelector((state) => state.auth.systemPrompt);
//state
const [userConfigNumber, setUserConfigNumber] = useLocalStorage(
"userConfigNumber",
"2"
);
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>
@ -27,6 +56,31 @@ const Settings = () => {
<FontAwesomeIcon icon={faArrowLeft} size="2x" />
</Link>
</div>
{/* 配置选择器 */}
<label
className="block text-gray-700 text-sm font-bold mb-2"
htmlFor="config-selector"
>
</label>
<select
id="config-selector"
className="mb-4 block appearance-none w-full bg-white border border-gray-400 hover:border-gray-500 px-4 py-2 pr-8 rounded shadow leading-tight focus:outline-none focus:shadow-outline"
onChange={(event) => {
const selectedConfig = CONFIG_OPTIONS[Number(event.target.value)];
dispatch(setApiKey(selectedConfig.apiKey));
dispatch(setUpsreamUrl(selectedConfig.upstreamUrl));
setUserConfigNumber(event.target.value);
console.log("userConfigNumber", userConfigNumber);
}}
value={userConfigNumber}
>
{CONFIG_OPTIONS.map((option, index) => (
<option key={index} value={index}>
{option.name}
</option>
))}
</select>
{/* api key */}
<div className="mb-4">
<label
@ -37,7 +91,7 @@ const Settings = () => {
</label>
<input
id="api-key"
type="text"
type="password"
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"
@ -69,7 +123,7 @@ const Settings = () => {
<textarea
id="system-prompt"
value={systemPrompt}
onChange={(event) => setSystemPrompt(event.target.value)}
onChange={(event) => dispatch(setSystemPrompt(event.target.value))}
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
rows={8}
/>

View File

@ -77,7 +77,7 @@ const sendMessageToOpenAI = async (
requestOptions
);
if (!response.ok || !response.body) {
throw new Error("Server responded with an error" + response);
throw new Error("");
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
@ -88,11 +88,11 @@ const sendMessageToOpenAI = async (
convertToSuperscript(editor);
updateBracketNumbersInDeltaKeepSelection(editor);
} catch (error) {
console.error("Error:", error);
// console.error("Error:", error);
// 如果有响应,返回响应的原始内容
if (response) {
const rawResponse = await response.text();
throw new Error(`Error: ${error.message}, Response: ${rawResponse}`);
throw new Error(`请求发生错误: ${error}, Response: ${rawResponse}`);
}
// 如果没有响应,只抛出错误
throw error;
@ -166,7 +166,8 @@ async function processResult(reader, decoder, editor) {
// 如果 jsonStr 以 "data: " 开头,就移除这个前缀
// 移除字符串首尾的空白字符
jsonStr = jsonStr.trim();
jsonStr = jsonStr.substring(6);
// jsonStr = jsonStr.substring(6);
jsonStr = jsonStr.replace("data:", "");
let dataObject = JSON.parse(jsonStr);
// console.log("dataObject", dataObject);
// 处理 dataObject 中的 content
@ -182,9 +183,16 @@ async function processResult(reader, decoder, editor) {
}
}
} catch (error) {
// console.error("Failed to parse JSON object:", jsonStr);
console.error("Error:", error);
break;
// 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}`);
}
}
} catch (error) {

View File

@ -1,94 +0,0 @@
/* /componens/plan.jsx */
'use client';
import { useState } from 'react'
function createMarkup(html) {
return {__html: html}
}
function formatPrice(price) {
return price / 100
}
function formatInterval(interval, intervalCount) {
return intervalCount > 1 ? `${intervalCount} ${interval}s` : interval
}
function IntervalSwitcher({ intervalValue, changeInterval }) {
return (
<div className="mt-6 flex justify-center items-center gap-4 text-sm text-gray-500">
<div data-plan-toggle="month">
Monthly
</div>
<div>
<label className="toggle relative inline-block">
<input
type="checkbox"
checked={intervalValue == 'year'}
onChange={(e) => changeInterval(e.target.checked ? 'year' : 'month')}
/>
<span className="slider absolute rounded-full bg-gray-300 shadow-md"></span>
</label>
</div>
<div data-plan-toggle="year">
Yearly
</div>
</div>
);
}
function Plan({ plan, subscription, intervalValue }) {
return (
<div
className={
'flex flex-col p-4 rounded-md border-solid border-2 border-gray-200'
+ (plan.interval !== intervalValue ? ' hidden' : '')
+ (subscription?.status !== 'expired' && subscription?.variantId == plan.variantId ? ' opacity-50' : '')
}
>
<div className="grow">
<h1 className="font-bold text-lg mb-1">{plan.variantName}</h1>
<div dangerouslySetInnerHTML={createMarkup(plan.description)}></div>
<div className="my-4">
<span className="text-2xl">${formatPrice(plan.price)}</span>
&nbsp;
<span className="text-gray-500">/{formatInterval(plan.interval, plan.intervalCount)}</span>
</div>
</div>
<div className="mt-4">
<a
href="#"
className="block text-center py-2 px-5 bg-amber-200 rounded-full font-bold text-amber-800 shadow-md shadow-gray-300/30 select-none"
>
Sign up
</a>
</div>
</div>
)
}
export default function Plans({ plans, subscription }) {
const [intervalValue, setIntervalValue] = useState('month')
return (
<>
<IntervalSwitcher intervalValue={intervalValue} changeInterval={setIntervalValue} />
<div className="mt-5 grid gap-6 sm:grid-cols-2">
{plans.map(plan => (
<Plan plan={plan} subscription={subscription} intervalValue={intervalValue} key={plan.variantId} />
))}
</div>
<p className="mt-8 text-gray-400 text-sm text-center">
Payments are processed securely by Lemon Squeezy.
</p>
</>
)
}

219
package-lock.json generated
View File

@ -28,10 +28,12 @@
"postcss": "8.4.31",
"quill": "^1.3.7",
"quill-to-word": "^1.3.0",
"raw-body": "^2.5.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-quill": "^2.0.0",
"react-redux": "^9.1.0",
"react-transition-group": "^4.4.5",
"react-use": "^17.4.3",
"redux": "^5.0.1",
"redux-logger": "^3.0.6",
@ -50,6 +52,7 @@
"@types/node": "^20.3.1",
"@types/react": "^18.2.48",
"@types/react-dom": "18.2.5",
"@types/react-transition-group": "^4.4.10",
"@types/redux-logger": "^3.0.12",
"encoding": "^0.1.13"
}
@ -579,6 +582,15 @@
"@types/react": "*"
}
},
"node_modules/@types/react-transition-group": {
"version": "4.4.10",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz",
"integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==",
"dev": true,
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/redux-logger": {
"version": "3.0.12",
"resolved": "https://registry.npmjs.org/@types/redux-logger/-/redux-logger-3.0.12.tgz",
@ -814,6 +826,14 @@
"node": ">=10.16.0"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/call-bind": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz",
@ -1091,6 +1111,14 @@
"node": ">=0.4.0"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/didyoumean": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@ -1143,6 +1171,15 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz",
"integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ=="
},
"node_modules/dom-helpers": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
"dependencies": {
"@babel/runtime": "^7.8.7",
"csstype": "^3.0.2"
}
},
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@ -1522,6 +1559,21 @@
"node": ">= 0.4"
}
},
"node_modules/http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
"dependencies": {
"depd": "2.0.0",
"inherits": "2.0.4",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"toidentifier": "1.0.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/humanize-ms": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
@ -2406,6 +2458,31 @@
"url": "https://opencollective.com/ramda"
}
},
"node_modules/raw-body": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
"dependencies": {
"bytes": "3.1.2",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/raw-body/node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
@ -2474,6 +2551,21 @@
}
}
},
"node_modules/react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
"dependencies": {
"@babel/runtime": "^7.5.5",
"dom-helpers": "^5.0.1",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2"
},
"peerDependencies": {
"react": ">=16.6.0",
"react-dom": ">=16.6.0"
}
},
"node_modules/react-universal-interface": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/react-universal-interface/-/react-universal-interface-0.6.2.tgz",
@ -2664,8 +2756,7 @@
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"devOptional": true
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/sax": {
"version": "1.3.0",
@ -2739,6 +2830,11 @@
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@ -2892,6 +2988,14 @@
"stacktrace-gps": "^3.0.4"
}
},
"node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
@ -3158,6 +3262,14 @@
"resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
"integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ=="
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"engines": {
"node": ">=0.6"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
@ -3195,6 +3307,14 @@
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/update-browserslist-db": {
"version": "1.0.13",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
@ -3804,6 +3924,15 @@
"@types/react": "*"
}
},
"@types/react-transition-group": {
"version": "4.4.10",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz",
"integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==",
"dev": true,
"requires": {
"@types/react": "*"
}
},
"@types/redux-logger": {
"version": "3.0.12",
"resolved": "https://registry.npmjs.org/@types/redux-logger/-/redux-logger-3.0.12.tgz",
@ -3968,6 +4097,11 @@
"streamsearch": "^1.1.0"
}
},
"bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="
},
"call-bind": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz",
@ -4162,6 +4296,11 @@
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
},
"depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
},
"didyoumean": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@ -4206,6 +4345,15 @@
}
}
},
"dom-helpers": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
"requires": {
"@babel/runtime": "^7.8.7",
"csstype": "^3.0.2"
}
},
"eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@ -4481,6 +4629,18 @@
"function-bind": "^1.1.2"
}
},
"http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
"requires": {
"depd": "2.0.0",
"inherits": "2.0.4",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"toidentifier": "1.0.1"
}
},
"humanize-ms": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
@ -5075,6 +5235,27 @@
"resolved": "https://registry.npmjs.org/ramda/-/ramda-0.29.1.tgz",
"integrity": "sha512-OfxIeWzd4xdUNxlWhgFazxsA/nl3mS4/jGZI5n00uWOoSSFRhC1b6gl6xvmzUamgmqELraWp0J/qqVlXYPDPyA=="
},
"raw-body": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
"requires": {
"bytes": "3.1.2",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
},
"dependencies": {
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
}
}
},
"react": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
@ -5116,6 +5297,17 @@
"use-sync-external-store": "^1.0.0"
}
},
"react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
"requires": {
"@babel/runtime": "^7.5.5",
"dom-helpers": "^5.0.1",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2"
}
},
"react-universal-interface": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/react-universal-interface/-/react-universal-interface-0.6.2.tgz",
@ -5262,8 +5454,7 @@
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"devOptional": true
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"sax": {
"version": "1.3.0",
@ -5322,6 +5513,11 @@
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
},
"setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
},
"shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@ -5446,6 +5642,11 @@
"stacktrace-gps": "^3.0.4"
}
},
"statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="
},
"streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
@ -5637,6 +5838,11 @@
"resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
"integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ=="
},
"toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="
},
"tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
@ -5667,6 +5873,11 @@
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
},
"unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="
},
"update-browserslist-db": {
"version": "1.0.13",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",

View File

@ -27,10 +27,12 @@
"postcss": "8.4.31",
"quill": "^1.3.7",
"quill-to-word": "^1.3.0",
"raw-body": "^2.5.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-quill": "^2.0.0",
"react-redux": "^9.1.0",
"react-transition-group": "^4.4.5",
"react-use": "^17.4.3",
"redux": "^5.0.1",
"redux-logger": "^3.0.6",
@ -49,6 +51,7 @@
"@types/node": "^20.3.1",
"@types/react": "^18.2.48",
"@types/react-dom": "18.2.5",
"@types/react-transition-group": "^4.4.10",
"@types/redux-logger": "^3.0.12",
"encoding": "^0.1.13"
},

View File

@ -16,6 +16,7 @@ module.exports = {
"blue-gray": {
100: "#F0F4F8", // 这里使用你选择的颜色值
},
gold: "#FFD700",
},
},
},

View File

@ -1,14 +1,15 @@
import { Reference } from "@/utils/global";
import Quill from "quill";
import { animated, useSpring } from "@react-spring/web";
function getTextBeforeCursor(quill, length = 500) {
const cursorPosition = quill.getSelection().index;
function getTextBeforeCursor(quill: Quill, length = 500) {
const cursorPosition = quill.getSelection()!.index;
const start = Math.max(0, cursorPosition - length); // 确保开始位置不是负数
return quill.getText(start, cursorPosition - start);
}
function getNumberBeforeCursor(quill, length = 3000) {
const cursorPosition = quill.getSelection().index;
function getNumberBeforeCursor(quill: Quill, length = 3000) {
const cursorPosition = quill.getSelection()!.index;
const start = Math.max(0, cursorPosition - length); // 确保开始位置不是负数
const textBeforeCursor = quill.getText(start, cursorPosition - start);
@ -31,10 +32,10 @@ function getNumberBeforeCursor(quill, length = 3000) {
return 0;
}
function updateBracketNumbersInDelta(delta) {
function updateBracketNumbersInDelta(delta: any) {
let currentNumber = 1;
const updatedOps = delta.ops.map((op) => {
const updatedOps = delta.ops.map((op: any) => {
if (typeof op.insert === "string") {
return {
...op,
@ -48,9 +49,9 @@ function updateBracketNumbersInDelta(delta) {
}
function deleteReferenceNumberOrParagraph(
delta,
delta: any,
indexToRemove: number,
quill,
quill: Quill,
deleteParagraph: boolean
) {
const indexStr = `[${indexToRemove + 1}]`;
@ -63,7 +64,7 @@ function deleteReferenceNumberOrParagraph(
let delta = quill.clipboard.convert(htmlString);
return delta;
} else {
const updatedOps = delta.ops.flatMap((op, i) => {
const updatedOps = delta.ops.flatMap((op: any, i) => {
if (typeof op.insert === "string") {
const indexPos = op.insert.indexOf(indexStr);
if (indexPos !== -1) {
@ -131,7 +132,7 @@ function removeParagraphWithReference(
);
}
function updateBracketNumbersInDeltaKeepSelection(quill) {
function updateBracketNumbersInDeltaKeepSelection(quill: Quill) {
const selection = quill.getSelection();
const delta = quill.getContents();
const updatedDelta = updateBracketNumbersInDelta(delta);
@ -142,7 +143,7 @@ function updateBracketNumbersInDeltaKeepSelection(quill) {
}
export function delteIndexUpdateBracketNumbersInDeltaKeepSelection(
quill,
quill: Quill,
index: number,
rmPg: boolean
) {
@ -161,7 +162,7 @@ export function delteIndexUpdateBracketNumbersInDeltaKeepSelection(
}
}
function convertToSuperscript(quill) {
function convertToSuperscript(quill: Quill) {
const text = quill.getText();
const regex = /\[\d+\]/g; // 正则表达式匹配 "[数字]" 格式
let match;
@ -220,7 +221,7 @@ function formatAllReferencesForCopy(references: Reference[]): string {
.join("\n");
}
export function formatTextInEditor(editor) {
export function formatTextInEditor(editor: Quill) {
convertToSuperscript(editor);
updateBracketNumbersInDeltaKeepSelection(editor);
}

View File

@ -0,0 +1,13 @@
import { createClient } from "@supabase/supabase-js";
export const supabaseAdmin = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SECRET_KEY!,
{
auth: {
persistSession: false,
autoRefreshToken: false,
detectSessionInUrl: false,
},
}
);

View File

@ -0,0 +1,32 @@
CREATE POLICY "userpaper" ON public.user_paper FOR
INSERT WITH CHECK (auth.uid() = user_id);
-- Super base的表
create table public."user_paper" (
id bigint generated by default as identity,
created_at timestamp with time zone not null default now(),
user_id UUID REFERENCES auth.users NOT NULL,
paper_content character varying [] null,
paper_reference character varying [] null,
constraint userPaper_pkey primary key (id)
) tablespace pg_default;
-- trigger
BEGIN -- 检查用户是否是VIP
IF NOT EXISTS (
SELECT 1
FROM public.vip_statuses
WHERE user_id = new.user_id
AND is_vip
) THEN RAISE EXCEPTION 'User ID: %, is_vip: %, New, %',
new.user_id,
(
SELECT is_vip
FROM public.vip_statuses
WHERE user_id = new.user_id
),
NEW;
-- 如果用户不是VIP抛出异常
RAISE EXCEPTION 'Only VIP users are allowed to perform this operation.';
END IF;
-- 如果用户是VIP允许操作继续
RETURN NEW;
END;

View File

@ -0,0 +1,197 @@
import { SupabaseClient } from "@supabase/supabase-js";
// import { cookies } from "next/headers";
// import { createClient } from "@/utils/supabase/server";
import { Reference } from "@/utils/global";
//获取用户id
export async function getUser(supabase: SupabaseClient) {
const { data, error } = await supabase.auth.getSession();
if (data.session) {
const user = data.session.user;
if (user) {
// console.log("User UUID in getUser:", user.id);
return user;
} else {
console.log("No user in getUser");
return null;
}
} else {
console.log("No session in getUser");
return null;
}
}
//将论文保存到服务器
export async function submitPaper(
supabase: SupabaseClient,
editorContent: string,
references: Reference[],
paperNumber: string
) {
const user = await getUser(supabase);
if (user) {
try {
// console.log(user.id, editorContent, references);
const response = await fetch("/api/supa/data", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
userId: user.id,
paperContent: editorContent,
paperReference: references,
paperNumber,
}),
});
const data = await response.json();
// 处理响应数据
console.log(
"Response data in submitPaper:",
data,
`此次更新的是第${paperNumber}篇论文`
);
return data;
} catch (error) {
// 错误处理
console.error("Error submitting paper in submitPaper:", error);
}
} else {
console.log(
"No user found. User must be logged in to submit a paper. in submitPaper"
);
}
}
//添加某指定用户id下的论文
//删除指定用户下paperNumber的论文
export async function deletePaper(
supabase: SupabaseClient,
paperNumber: string
) {
const user = await getUser(supabase);
if (user) {
try {
const response = await fetch("/api/supa/data/delete", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
userId: user.id,
paperNumber,
}),
});
const data = await response.json();
// 处理响应数据
console.log("Response data in deletePaper:", data);
return data;
} catch (error) {
// 错误处理
console.error("Error deleting paper in deletePaper:", error);
}
} else {
console.log(
"No user found. User must be logged in to delete a paper. in deletePaper"
);
}
}
// 获取用户论文的序号
// export async function getUserPaperNumbers(
// userId: string,
// supabase: SupabaseClient
// ) {
// const { data, error } = await supabase
// .from("user_paper") // 指定表名
// .select("paper_number") // 仅选择paper_number列
// .eq("user_id", userId); // 筛选特定user_id的记录
// if (error) {
// console.error("查询出错", error);
// return null;
// }
// // 返回查询结果,即所有论文的序号
// return data.map((paper) => paper.paper_number);
// }
export async function getUserPaperNumbers(userId: string) {
try {
const response = await fetch("/api/supa/paper-numbers", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ userId }),
});
const data = await response.json();
if (response.ok) {
// 返回查询结果,即所有论文的序号
console.log("获取到的用户论文数量:", data);
return data.map((paper: any) => paper.paper_number);
} else {
console.error("获取用户论文数量时发生错误:", data);
return null;
}
} catch (error) {
console.error("请求出错", error);
return null;
}
}
// 获取用户指定序号论文的内容
// export async function getUserPaper(
// userId: string,
// paperNumber: string,
// supabase: SupabaseClient
// ) {
// const { data, error } = await supabase
// .from("user_paper") // 指定表名
// .select("paper_content,paper_reference") // 仅选择paper_content列
// .eq("user_id", userId) // 筛选特定user_id的记录
// .eq("paper_number", paperNumber)
// .single(); // 筛选特定paper_number的记录
// if (error) {
// console.error("查询出错", error);
// return null;
// }
// // 返回查询结果,即指定论文的内容
// return data;
// }
export async function getUserPaper(userId: string, paperNumber: string) {
try {
const response = await fetch("/api/supa/user-papers", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ userId, paperNumber }),
});
const data = await response.json();
if (response.ok) {
console.log("获取到的用户论文数据:", data);
return data; // 返回查询结果
} else {
console.error("获取用户论文时发生错误:", data);
return null;
}
} catch (error) {
console.error("请求出错", error);
return null;
}
}
//Super base的表
// create table
// public."user_paper" (
// id bigint generated by default as identity,
// created_at timestamp with time zone not null default now(),
// user_id UUID REFERENCES auth.users NOT NULL,
// paper_content character varying[] null,
// paper_reference character varying[] null,
// constraint userPaper_pkey primary key (id)
// ) tablespace pg_default;
//获取和用户ID相关联的论文

View File

@ -0,0 +1,174 @@
import { SupabaseClient } from "@supabase/supabase-js";
// import { cookies } from "next/headers";
// import { createClient } from "@/utils/supabase/server";
import { redirect } from "next/navigation";
import { Reference } from "@/utils/global";
//supabase
const supabase = createClient();
import { createClient } from "@/utils/supabase/client";
//获取用户id
export async function getUser() {
const { data, error } = await supabase.auth.getSession();
if (data.session) {
const user = data.session.user;
if (user) {
// console.log("User UUID in getUser:", user.id);
return user;
} else {
console.log("No user in getUser");
return null;
}
} else {
console.log("No session in getUser");
return null;
}
}
//将论文保存到服务器
export async function submitPaper(
supabase: SupabaseClient,
editorContent?: string, // 使得editorContent成为可选参数
references?: Reference[], // 使得references成为可选参数
paperNumber = "1"
) {
const user = await getUser(supabase);
if (user) {
try {
// 构造请求体,只包含提供的参数
const requestBody: any = {
userId: user.id,
paperNumber,
};
if (editorContent !== undefined) {
requestBody.paperContent = editorContent;
}
if (references !== undefined) {
requestBody.paperReference = references;
}
const response = await fetch("/api/supa/data", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(requestBody),
});
const data = await response.json();
console.log(
"Response data in submitPaper:",
data,
`此次更新的是第${paperNumber}篇论文,` +
`${editorContent !== undefined ? "更新内容为" + editorContent : ""}` +
`${
references !== undefined
? "更新引用为" + JSON.stringify(references)
: ""
}`
);
return data;
} catch (error) {
console.error("Error submitting paper in submitPaper:", error);
}
} else {
console.log(
"No user found. User must be logged in to submit a paper. in submitPaper"
);
}
}
//添加某指定用户id下的论文
//删除指定用户下paperNumber的论文
export async function deletePaper(
supabase: SupabaseClient,
userId: string,
paperNumber: string
) {
const { data, error } = await supabase
.from("user_paper")
.delete()
.eq("user_id", userId)
.eq("paper_number", paperNumber);
console.log("删除的数据", data);
if (error) {
console.error("删除出错", error);
return null;
}
return data;
}
//获取用户论文
export async function getUserPapers(userId: string, supabase: SupabaseClient) {
const { data, error } = await supabase
.from("user_paper") // 指定表名
.select("*") // 选择所有列
.eq("user_id", userId); // 筛选特定user_id的记录
if (error) {
console.error("查询出错", error);
return null;
}
return data; // 返回查询结果
}
// 获取用户论文的序号
export async function getUserPaperNumbers(
userId: string,
supabase: SupabaseClient
) {
const { data, error } = await supabase
.from("user_paper") // 指定表名
.select("paper_number") // 仅选择paper_number列
.eq("user_id", userId); // 筛选特定user_id的记录
if (error) {
console.error("查询出错", error);
return null;
}
console.log("获取到的用户论文数量:", data);
// 返回查询结果,即所有论文的序号
return data.map((paper) => paper.paper_number);
}
// 获取用户指定序号论文的内容
export async function getUserPaper(
userId: string,
paperNumber: string,
supabase: SupabaseClient
) {
const { data, error } = await supabase
.from("user_paper") // 指定表名
.select("paper_content,paper_reference") // 仅选择paper_content列
.eq("user_id", userId) // 筛选特定user_id的记录
.eq("paper_number", paperNumber)
.single(); // 筛选特定paper_number的记录
if (error) {
console.error("查询出错", error);
return null;
}
// 返回查询结果,即指定论文的内容
return data;
}
// 使用Supabase客户端实例来查询vip_statuses表
export async function fetchUserVipStatus(userId: string) {
const { data, error } = await supabase
.from("vip_statuses")
.select("is_vip")
.eq("user_id", userId)
.single();
if (error) {
console.error("Error fetching VIP status:", error);
return false;
}
if ("is_vip" in data) {
console.log("VIP status:", data.is_vip);
return data.is_vip;
} else {
return false;
}
}