commit
729de74653
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -35,4 +35,5 @@ yarn-error.log*
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
.env
|
||||||
|
|
92
app/api/lemon/callback/route.ts
Normal file
92
app/api/lemon/callback/route.ts
Normal 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 状态已更新:" });
|
||||||
|
}
|
51
app/api/supa/data/route.ts
Normal file
51
app/api/supa/data/route.ts
Normal 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",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
63
app/api/supa/paper-numbers/route.ts
Normal file
63
app/api/supa/paper-numbers/route.ts
Normal 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",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
43
app/api/supa/user-papers/route.ts
Normal file
43
app/api/supa/user-papers/route.ts
Normal 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",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
0
app/api/supa/vip/route.ts
Normal file
0
app/api/supa/vip/route.ts
Normal file
|
@ -76,3 +76,36 @@
|
||||||
.vip-icon {
|
.vip-icon {
|
||||||
animation: flash 1s linear infinite;
|
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; /* 确保悬浮层在其他内容之上 */
|
||||||
|
/* 可以添加其他样式来美化组件,如背景色、阴影等 */
|
||||||
|
}
|
||||||
|
|
19
app/page.tsx
19
app/page.tsx
|
@ -1,5 +1,5 @@
|
||||||
import DeployButton from "../components/DeployButton";
|
import PaperListButtonWrapper from "@/components/PaperListButtonWrapper";
|
||||||
import AuthButton from "../components/AuthButton";
|
import AuthButton from "@/components/AuthButton";
|
||||||
import { createClient } from "@/utils/supabase/server";
|
import { createClient } from "@/utils/supabase/server";
|
||||||
import ConnectSupabaseSteps from "@/components/ConnectSupabaseSteps";
|
import ConnectSupabaseSteps from "@/components/ConnectSupabaseSteps";
|
||||||
import SignUpUserSteps from "@/components/SignUpUserSteps";
|
import SignUpUserSteps from "@/components/SignUpUserSteps";
|
||||||
|
@ -9,11 +9,10 @@ import QuillWrapper from "./QuillWrapper";
|
||||||
// import TinyEditor from "../components/TinyEditor";
|
// import TinyEditor from "../components/TinyEditor";
|
||||||
// import SEditor from "../components/SlateEditor";
|
// import SEditor from "../components/SlateEditor";
|
||||||
import SettingsLink from "@/components/SettingsLink";
|
import SettingsLink from "@/components/SettingsLink";
|
||||||
import { ErrorBoundary } from "next/dist/client/components/error-boundary";
|
import PaperManagementWrapper from "@/components/PaperManagementWrapper";
|
||||||
// import React, { useState, useEffect, useRef } from "react";
|
|
||||||
|
|
||||||
// import Error from "@/app/global-error";
|
// import Error from "@/app/global-error";
|
||||||
export default async function Index() {
|
export default function Index() {
|
||||||
const cookieStore = cookies();
|
const cookieStore = cookies();
|
||||||
|
|
||||||
const canInitSupabaseClient = () => {
|
const canInitSupabaseClient = () => {
|
||||||
|
@ -30,18 +29,18 @@ export default async function Index() {
|
||||||
const isSupabaseConnected = canInitSupabaseClient();
|
const isSupabaseConnected = canInitSupabaseClient();
|
||||||
|
|
||||||
return (
|
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">
|
<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">
|
<div className="w-full max-w-4xl flex justify-between items-center p-3 text-sm">
|
||||||
<DeployButton />
|
{/* <DeployButton /> */}
|
||||||
|
{/* 用来表示是否显示论文列表页 */}
|
||||||
|
<PaperListButtonWrapper />
|
||||||
{isSupabaseConnected && <AuthButton />}
|
{isSupabaseConnected && <AuthButton />}
|
||||||
<SettingsLink />
|
<SettingsLink />
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
{/* <ErrorBoundary fallback={<Error />}> */}
|
<PaperManagementWrapper />
|
||||||
<QuillWrapper />
|
<QuillWrapper />
|
||||||
{/* </ErrorBoundary> */}
|
|
||||||
|
|
||||||
<footer className="w-full border-t border-t-foreground/10 p-8 flex justify-center text-center text-xs">
|
<footer className="w-full border-t border-t-foreground/10 p-8 flex justify-center text-center text-xs">
|
||||||
<p>
|
<p>
|
||||||
<a
|
<a
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { useDispatch, TypedUseSelectorHook, useSelector } from "react-redux";
|
||||||
import { persistReducer } from "redux-persist";
|
import { persistReducer } from "redux-persist";
|
||||||
// import storage from "redux-persist/lib/storage";
|
// import storage from "redux-persist/lib/storage";
|
||||||
import { authReducer } from "./slices/authSlice";
|
import { authReducer } from "./slices/authSlice";
|
||||||
|
import { stateReducer } from "./slices/stateSlice";
|
||||||
import storage from "./customStorage";
|
import storage from "./customStorage";
|
||||||
import logger from "redux-logger";
|
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({
|
const rootReducer = combineReducers({
|
||||||
auth: persistReducer(authPersistConfig, authReducer),
|
auth: persistReducer(authPersistConfig, authReducer),
|
||||||
|
state: persistReducer(statePersistConfig, stateReducer),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const store = configureStore({
|
export const store = configureStore({
|
||||||
reducer: rootReducer,
|
reducer: rootReducer,
|
||||||
middleware: (getDefaultMiddleware) =>
|
middleware: (getDefaultMiddleware) =>
|
||||||
getDefaultMiddleware({ serializableCheck: false }).concat(logger),
|
getDefaultMiddleware({ serializableCheck: false }).concat(logger), //.concat(logger)
|
||||||
});
|
});
|
||||||
|
|
||||||
export type RootState = ReturnType<typeof store.getState>;
|
export type RootState = ReturnType<typeof store.getState>;
|
||||||
|
|
|
@ -6,20 +6,33 @@ export interface APIState {
|
||||||
editorContent: string;
|
editorContent: string;
|
||||||
upsreamUrl: string;
|
upsreamUrl: string;
|
||||||
systemPrompt: string;
|
systemPrompt: string;
|
||||||
|
showPaperManagement: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: APIState = {
|
const initialState: APIState = {
|
||||||
apiKey: "sess-L6DwIB7N859iQLWfNBTaPsmkErqZrjoXVk6m7BmA",
|
apiKey: "sk-aiHrrRLYUUelHstX69E9484509254dBf92061d6744FfFaD1", //sk-ffe19ebe9fa44d00884330ff1c18cf82
|
||||||
referencesRedux: [],
|
referencesRedux: [],
|
||||||
editorContent: "",
|
editorContent: "",
|
||||||
upsreamUrl: "https://api.liuweiqing.top", //https://api.openai.com https://one.caifree.com https://chatserver.3211000.xyz
|
upsreamUrl: "https://one.caifree.com", //https://api.openai.com https://one.caifree.com https://chatserver.3211000.xyz https://api.deepseek.com
|
||||||
systemPrompt: "",
|
systemPrompt: `作为论文写作助手,您的主要任务是根据用户提供的研究主题和上下文,以及相关的研究论文,来撰写和完善学术论文。在撰写过程中,请注意以下要点:
|
||||||
|
1.学术格式:请采用标准的学术论文格式进行写作,包括清晰的段落结构、逻辑严谨的论点展开,以及恰当的专业术语使用。
|
||||||
|
2.文献引用:只引用与主题紧密相关的论文。在引用文献时,文末应使用方括号内的数字来标注引用来源,如 [1]。。请确保每个引用在文章中都有其对应的编号,*无需在文章末尾提供参考文献列表*。*每个文献对应的序号只应该出现一次,比如说引用了第一篇文献文中就只能出现一次[1]*。
|
||||||
|
3.忽略无关文献:对于与主题无关的论文,请不要包含在您的写作中。只关注对理解和阐述主题有实质性帮助的资料。
|
||||||
|
4.来源明确:在文章中,清楚地指出每个引用的具体来源。引用的信息应准确无误,确保读者能够追溯到原始文献。
|
||||||
|
5.使用中文完成回答,不超过三百字
|
||||||
|
6.只能对给出的文献进行引用,坚决不能虚构文献。
|
||||||
|
返回格式举例:
|
||||||
|
在某个方面,某论文实现了以下突破...[1],在另一篇论文中,研究了...[2]`,
|
||||||
|
showPaperManagement: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const authSlice = createSlice({
|
export const authSlice = createSlice({
|
||||||
name: "auth",
|
name: "auth",
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
|
setShowPaperManagement: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.showPaperManagement = action.payload;
|
||||||
|
},
|
||||||
setApiKey: (state, action: PayloadAction<string>) => {
|
setApiKey: (state, action: PayloadAction<string>) => {
|
||||||
state.apiKey = action.payload;
|
state.apiKey = action.payload;
|
||||||
},
|
},
|
||||||
|
@ -52,6 +65,9 @@ export const authSlice = createSlice({
|
||||||
clearReferencesRedux: (state) => {
|
clearReferencesRedux: (state) => {
|
||||||
state.referencesRedux = [];
|
state.referencesRedux = [];
|
||||||
},
|
},
|
||||||
|
setReferencesRedux: (state, action: PayloadAction<Reference[]>) => {
|
||||||
|
state.referencesRedux = action.payload;
|
||||||
|
},
|
||||||
swapReferencesRedux: (
|
swapReferencesRedux: (
|
||||||
state,
|
state,
|
||||||
action: PayloadAction<{ indexA: number; indexB: number }>
|
action: PayloadAction<{ indexA: number; indexB: number }>
|
||||||
|
@ -77,6 +93,7 @@ export const authSlice = createSlice({
|
||||||
|
|
||||||
// Action creators are generated for each case reducer function
|
// Action creators are generated for each case reducer function
|
||||||
export const {
|
export const {
|
||||||
|
setShowPaperManagement,
|
||||||
setApiKey,
|
setApiKey,
|
||||||
setUpsreamUrl,
|
setUpsreamUrl,
|
||||||
addReferenceRedux,
|
addReferenceRedux,
|
||||||
|
@ -84,6 +101,7 @@ export const {
|
||||||
removeReferenceRedux,
|
removeReferenceRedux,
|
||||||
clearReferencesRedux,
|
clearReferencesRedux,
|
||||||
setEditorContent,
|
setEditorContent,
|
||||||
|
setReferencesRedux,
|
||||||
setSystemPrompt,
|
setSystemPrompt,
|
||||||
swapReferencesRedux,
|
swapReferencesRedux,
|
||||||
} = authSlice.actions;
|
} = authSlice.actions;
|
||||||
|
|
49
app/store/slices/stateSlice.ts
Normal file
49
app/store/slices/stateSlice.ts
Normal 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;
|
|
@ -1,7 +1,10 @@
|
||||||
|
// "use server";
|
||||||
|
|
||||||
import { createClient } from "@/utils/supabase/server";
|
import { createClient } from "@/utils/supabase/server";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
// import { signOut } from "@/utils/supabase/serversignout";
|
||||||
|
|
||||||
export default async function AuthButton() {
|
export default async function AuthButton() {
|
||||||
const cookieStore = cookies();
|
const cookieStore = cookies();
|
||||||
|
@ -17,15 +20,16 @@ export default async function AuthButton() {
|
||||||
const cookieStore = cookies();
|
const cookieStore = cookies();
|
||||||
const supabase = createClient(cookieStore);
|
const supabase = createClient(cookieStore);
|
||||||
await supabase.auth.signOut();
|
await supabase.auth.signOut();
|
||||||
|
|
||||||
return redirect("/login");
|
return redirect("/login");
|
||||||
};
|
};
|
||||||
|
|
||||||
return user ? (
|
return user ? (
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
Hey, {user.email}!
|
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
|
VIP
|
||||||
</div>
|
</div> */}
|
||||||
<form action={signOut}>
|
<form action={signOut}>
|
||||||
<button className="py-2 px-4 rounded-md no-underline bg-btn-background hover:bg-btn-background-hover">
|
<button className="py-2 px-4 rounded-md no-underline bg-btn-background hover:bg-btn-background-hover">
|
||||||
Logout
|
Logout
|
||||||
|
|
16
components/BuyVipButton.tsx
Normal file
16
components/BuyVipButton.tsx
Normal 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;
|
|
@ -53,18 +53,15 @@ async function getArxivPapers(
|
||||||
// 你可以在这里处理数据
|
// 你可以在这里处理数据
|
||||||
result = extractArxivData(result);
|
result = extractArxivData(result);
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
if (error.response) {
|
throw new Error(
|
||||||
// 请求已发送,但服务器响应的状态码不在 2xx 范围内
|
`Error fetching data from Arxiv API:${JSON.stringify(
|
||||||
console.error("Error fetching data: ", error.response.data);
|
error.response,
|
||||||
} else if (error.request) {
|
null,
|
||||||
// 请求已发送,但没有收到响应
|
2
|
||||||
console.error("No response received: ", error.request);
|
)}`
|
||||||
} else {
|
);
|
||||||
// 发送请求时出现错误
|
// return null;
|
||||||
console.error("Error setting up the request: ", error.message);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -53,7 +53,7 @@ async function getPubMedPaperDetails(idList: IDList) {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
throw new Error(`${response.text()}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.text(); // 获取响应文本
|
const data = await response.text(); // 获取响应文本
|
||||||
|
@ -152,9 +152,9 @@ async function fetchPubMedData(query: string, year: number, limit: number) {
|
||||||
console.log("fetchPubMedData", paperDetails); // 处理或显示文章详情
|
console.log("fetchPubMedData", paperDetails); // 处理或显示文章详情
|
||||||
return paperDetails;
|
return paperDetails;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (error) {
|
||||||
console.error("未搜索到文献");
|
//这里无法起作用因为pubmed不会返回400系错误
|
||||||
throw new Error("未搜索到文献");
|
throw new Error(`未搜索到文献: ${error}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import {getRandomOffset} from "@/utils/others/quillutils"
|
import { getRandomOffset } from "@/utils/others/quillutils";
|
||||||
interface Author {
|
interface Author {
|
||||||
authorId: string;
|
authorId: string;
|
||||||
name: 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 url = `https://api.semanticscholar.org/graph/v1/paper/search`;
|
||||||
const response = await axios.get(url, {
|
const response = await axios.get(url, {
|
||||||
headers: {
|
headers: {
|
||||||
'x-api-key': process.env.NEXT_PUBLIC_SEMANTIC_API_KEY,
|
"x-api-key": process.env.NEXT_PUBLIC_SEMANTIC_API_KEY,
|
||||||
},
|
},
|
||||||
params: {
|
params: {
|
||||||
query: query,
|
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);
|
const authorNames = paper.authors.map((author) => author.name);
|
||||||
|
|
||||||
|
@ -43,13 +43,19 @@ async function getSemanticPapers(query: string, year: string, limit = 2) {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
return papers;
|
return papers;
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error("Error fetching data from Semantic Scholar API:", error);
|
// console.error("Error fetching data from Semantic Scholar API:", error);
|
||||||
return null; // 或根据需要处理错误
|
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) => {
|
// fetchSemanticPapers("covid", 50, 2, "2015-2023").then((data) => {
|
||||||
// console.log(data);
|
// console.log(data);
|
||||||
|
|
32
components/PaperListButton.tsx
Normal file
32
components/PaperListButton.tsx
Normal 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";
|
12
components/PaperListButtonWrapper.tsx
Normal file
12
components/PaperListButtonWrapper.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
201
components/PaperManagement.tsx
Normal file
201
components/PaperManagement.tsx
Normal 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;
|
12
components/PaperManagementWrapper.tsx
Normal file
12
components/PaperManagementWrapper.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -8,14 +8,18 @@ interface SweetAlertComponentProps {
|
||||||
removeReferenceUpdateIndex: (index: number, rmPg: boolean) => void;
|
removeReferenceUpdateIndex: (index: number, rmPg: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ParagraphDeleteButton: React.FC<SweetAlertComponentProps> = ({
|
const ParagraphDeleteButton: React.FC<any> = ({
|
||||||
index,
|
index,
|
||||||
removeReferenceUpdateIndex,
|
removeReferenceUpdateIndex,
|
||||||
|
isRemovePaper = false,
|
||||||
|
title = "需要同时删除与文献相关的整个段落吗?",
|
||||||
|
text = "根据周围的换行符来判断是否是同一个段落",
|
||||||
}) => {
|
}) => {
|
||||||
|
//这里传递函数的时候应该把参数先提前弄好 2.7
|
||||||
const showAlert = async () => {
|
const showAlert = async () => {
|
||||||
const result = await Swal.fire({
|
const result = await Swal.fire({
|
||||||
title: "需要同时删除与文献相关的整个段落吗?",
|
title: title,
|
||||||
text: "根据周围的换行符来判断是否是同一个段落",
|
text: text,
|
||||||
icon: "warning",
|
icon: "warning",
|
||||||
showCancelButton: true,
|
showCancelButton: true,
|
||||||
confirmButtonColor: "#3085d6",
|
confirmButtonColor: "#3085d6",
|
||||||
|
@ -23,10 +27,14 @@ const ParagraphDeleteButton: React.FC<SweetAlertComponentProps> = ({
|
||||||
confirmButtonText: "Yes, delete it!",
|
confirmButtonText: "Yes, delete it!",
|
||||||
});
|
});
|
||||||
if (result.isConfirmed) {
|
if (result.isConfirmed) {
|
||||||
removeReferenceUpdateIndex(index, true);
|
if (isRemovePaper) {
|
||||||
|
removeReferenceUpdateIndex(index, true);
|
||||||
|
} else {
|
||||||
|
removeReferenceUpdateIndex();
|
||||||
|
}
|
||||||
// Swal.fire("Deleted!", "Your file has been deleted.", "success");
|
// Swal.fire("Deleted!", "Your file has been deleted.", "success");
|
||||||
} else {
|
} else {
|
||||||
removeReferenceUpdateIndex(index, false);
|
if (!isRemovePaper) removeReferenceUpdateIndex(index, false);
|
||||||
// Swal.fire("Cancelled", "Your imaginary file is safe :)", "error");
|
// Swal.fire("Cancelled", "Your imaginary file is safe :)", "error");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -27,8 +27,18 @@ import {
|
||||||
addReferencesRedux,
|
addReferencesRedux,
|
||||||
setEditorContent,
|
setEditorContent,
|
||||||
} from "@/app/store/slices/authSlice";
|
} from "@/app/store/slices/authSlice";
|
||||||
|
import { setContentUpdatedFromNetwork } from "@/app/store/slices/stateSlice";
|
||||||
//类型声明
|
//类型声明
|
||||||
import { Reference } from "@/utils/global";
|
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 = [
|
const toolbarOptions = [
|
||||||
["bold", "italic", "underline", "strike"], // 加粗、斜体、下划线和删除线
|
["bold", "italic", "underline", "strike"], // 加粗、斜体、下划线和删除线
|
||||||
|
@ -55,6 +65,12 @@ const QEditor = () => {
|
||||||
const apiKey = useAppSelector((state: any) => state.auth.apiKey);
|
const apiKey = useAppSelector((state: any) => state.auth.apiKey);
|
||||||
const upsreamUrl = useAppSelector((state: any) => state.auth.upsreamUrl);
|
const upsreamUrl = useAppSelector((state: any) => state.auth.upsreamUrl);
|
||||||
const [quill, setQuill] = useState<Quill | null>(null);
|
const [quill, setQuill] = useState<Quill | null>(null);
|
||||||
|
const contentUpdatedFromNetwork = useAppSelector(
|
||||||
|
(state) => state.state.contentUpdatedFromNetwork
|
||||||
|
);
|
||||||
|
//vip状态
|
||||||
|
const isVip = useAppSelector((state) => state.state.isVip);
|
||||||
|
|
||||||
//询问ai,用户输入
|
//询问ai,用户输入
|
||||||
const [userInput, setUserInput] = useState("robot");
|
const [userInput, setUserInput] = useState("robot");
|
||||||
//quill编辑器鼠标位置
|
//quill编辑器鼠标位置
|
||||||
|
@ -65,20 +81,24 @@ const QEditor = () => {
|
||||||
const editor = useRef<Quill | null>(null);
|
const editor = useRef<Quill | null>(null);
|
||||||
// 选择论文来源
|
// 选择论文来源
|
||||||
const [selectedSource, setSelectedSource] = useLocalStorage(
|
const [selectedSource, setSelectedSource] = useLocalStorage(
|
||||||
"semanticScholar",
|
"学术引擎",
|
||||||
"semanticScholar"
|
"semanticScholar"
|
||||||
); // 默认选项
|
); // 默认选项
|
||||||
//选择语言模型
|
//选择语言模型
|
||||||
const [selectedModel, setSelectedModel] = useLocalStorage(
|
const [selectedModel, setSelectedModel] = useLocalStorage(
|
||||||
"gpt3.5",
|
"gpt语言模型",
|
||||||
"deepseek-chat"
|
"gpt-4"
|
||||||
); // 默认选项
|
); // 默认选项
|
||||||
//redux
|
//redux
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const references = useAppSelector((state) => state.auth.referencesRedux);
|
const references = useAppSelector((state) => state.auth.referencesRedux);
|
||||||
const editorContent = useAppSelector((state) => state.auth.editorContent); // 从 Redux store 中获取编辑器内容
|
const editorContent = useAppSelector((state) => state.auth.editorContent); // 从 Redux store 中获取编辑器内容
|
||||||
const systemPrompt = useAppSelector((state) => state.auth.systemPrompt);
|
const systemPrompt = useAppSelector((state) => state.auth.systemPrompt);
|
||||||
|
const paperNumberRedux = useAppSelector(
|
||||||
|
(state) => state.state.paperNumberRedux
|
||||||
|
);
|
||||||
|
//supabase
|
||||||
|
const supabase = createClient();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isMounted.current) {
|
if (!isMounted.current) {
|
||||||
editor.current = new Quill("#editor", {
|
editor.current = new Quill("#editor", {
|
||||||
|
@ -114,7 +134,6 @@ const QEditor = () => {
|
||||||
const range = editor.current!.getSelection();
|
const range = editor.current!.getSelection();
|
||||||
if (range && range.length === 0 && editor.current) {
|
if (range && range.length === 0 && editor.current) {
|
||||||
const [leaf, offset] = editor.current.getLeaf(range.index);
|
const [leaf, offset] = editor.current.getLeaf(range.index);
|
||||||
// console.log("leaf", leaf);
|
|
||||||
if (leaf.text) {
|
if (leaf.text) {
|
||||||
const textWithoutSpaces = leaf.text.replace(/\s+/g, ""); // 去掉所有空格
|
const textWithoutSpaces = leaf.text.replace(/\s+/g, ""); // 去掉所有空格
|
||||||
if (/^\[\d+\]$/.test(textWithoutSpaces)) {
|
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(() => {
|
useEffect(() => {
|
||||||
if (quill) {
|
if (quill) {
|
||||||
// 设置监听器以处理内容变化
|
// 设置监听器以处理内容变化
|
||||||
quill.on("text-change", function (delta, oldDelta, source) {
|
quill.on("text-change", handleTextChange);
|
||||||
if (source === "user") {
|
// 清理函数
|
||||||
// 获取编辑器内容
|
return () => {
|
||||||
const content = quill.root.innerHTML; // 或 quill.getText(),或 quill.getContents()
|
quill.off("text-change", handleTextChange);
|
||||||
|
};
|
||||||
// 保存到 localStorage
|
|
||||||
// localStorage.setItem("quillContent", content);
|
|
||||||
dispatch(setEditorContent(content)); // 更新 Redux store
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
convertToSuperscript(quill);
|
|
||||||
}, 0); // 延迟 0 毫秒,即将函数放入事件队列的下一个循环中执行,不然就会因为在改变文字触发整个函数时修改文本内容造成无法找到光标位置
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, [quill, dispatch]);
|
}, [quill, dispatch, paperNumberRedux]);
|
||||||
|
|
||||||
// 处理用户输入变化
|
// 处理用户输入变化
|
||||||
const handleInputChange = (event) => {
|
const handleInputChange = (event: any) => {
|
||||||
setUserInput(event.target.value);
|
setUserInput(event.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -237,6 +290,9 @@ const QEditor = () => {
|
||||||
.join("");
|
.join("");
|
||||||
} else if (selectedSource === "pubmed") {
|
} else if (selectedSource === "pubmed") {
|
||||||
rawData = await fetchPubMedData(topic, 2020, 2);
|
rawData = await fetchPubMedData(topic, 2020, 2);
|
||||||
|
if (!rawData) {
|
||||||
|
throw new Error("未搜索到文献 from PubMed.");
|
||||||
|
}
|
||||||
newReferences = rawData.map((entry) => ({
|
newReferences = rawData.map((entry) => ({
|
||||||
id: entry.id, // 文章的 PubMed ID
|
id: entry.id, // 文章的 PubMed ID
|
||||||
title: entry.title, // 文章的标题
|
title: entry.title, // 文章的标题
|
||||||
|
@ -257,6 +313,7 @@ const QEditor = () => {
|
||||||
})
|
})
|
||||||
.join("");
|
.join("");
|
||||||
}
|
}
|
||||||
|
//在对应的位置添加文献
|
||||||
const nearestNumber = getNumberBeforeCursor(quill);
|
const nearestNumber = getNumberBeforeCursor(quill);
|
||||||
dispatch(
|
dispatch(
|
||||||
addReferencesRedux({
|
addReferencesRedux({
|
||||||
|
@ -287,10 +344,19 @@ const QEditor = () => {
|
||||||
// 重新获取更新后的内容并更新 Redux store
|
// 重新获取更新后的内容并更新 Redux store
|
||||||
const updatedContent = quill.root.innerHTML;
|
const updatedContent = quill.root.innerHTML;
|
||||||
dispatch(setEditorContent(updatedContent));
|
dispatch(setEditorContent(updatedContent));
|
||||||
|
if (isVip) {
|
||||||
|
//在云端同步supabase
|
||||||
|
const data = await submitPaper(
|
||||||
|
supabase,
|
||||||
|
updatedContent,
|
||||||
|
references,
|
||||||
|
paperNumberRedux
|
||||||
|
);
|
||||||
|
}
|
||||||
} catch (error) {
|
} 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
|
AI Write
|
||||||
</button>
|
</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
|
<button
|
||||||
onClick={() => paper2AI(userInput)}
|
onClick={() => paper2AI(userInput)}
|
||||||
className="bg-yellow-500 hover:bg-yellow-600 text-white font-bold py-2 px-4 mr-2 rounded"
|
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)}
|
onChange={(e) => setSelectedModel(e.target.value)}
|
||||||
className=" border border-gray-300 bg-white py-2 px-3 rounded leading-tight focus:outline-none focus:bg-white focus:border-gray-500"
|
className=" border border-gray-300 bg-white py-2 px-3 rounded leading-tight focus:outline-none focus:bg-white focus:border-gray-500"
|
||||||
>
|
>
|
||||||
<option value="gpt-3.5-turbo">gpt3.5</option>
|
<option value="gpt-3.5-turbo">gpt-3.5-turbo</option>
|
||||||
<option value="gpt-4">gpt4</option>
|
<option value="gpt-4">gpt-4</option>
|
||||||
<option value="deepseek-chat">deepseek-chat</option>
|
<option value="deepseek-chat">deepseek-chat</option>
|
||||||
{/* 其他来源网站 */}
|
{/* 其他来源网站 */}
|
||||||
</select>
|
</select>
|
||||||
{/* 用户输入自己的API key */}
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => formatTextInEditor(quill)} // 假设 updateIndex 是处理更新操作的函数
|
onClick={() => formatTextInEditor(quill)} // 假设 updateIndex 是处理更新操作的函数
|
||||||
className="bg-gray-300 hover:bg-gray-400 text-black font-bold py-2 px-4 rounded"
|
className="bg-gray-300 hover:bg-gray-400 text-black font-bold py-2 px-4 rounded"
|
||||||
|
|
|
@ -18,7 +18,9 @@ import {
|
||||||
clearReferencesRedux,
|
clearReferencesRedux,
|
||||||
swapReferencesRedux,
|
swapReferencesRedux,
|
||||||
} from "@/app/store/slices/authSlice";
|
} from "@/app/store/slices/authSlice";
|
||||||
|
//supabase
|
||||||
|
import { submitPaper } from "@/utils/supabase/supabaseutils";
|
||||||
|
import { createClient } from "@/utils/supabase/client";
|
||||||
type ReferenceListProps = {
|
type ReferenceListProps = {
|
||||||
editor: any;
|
editor: any;
|
||||||
};
|
};
|
||||||
|
@ -33,6 +35,11 @@ function ReferenceList({ editor }: ReferenceListProps) {
|
||||||
//redux
|
//redux
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const references = useAppSelector((state) => state.auth.referencesRedux);
|
const references = useAppSelector((state) => state.auth.referencesRedux);
|
||||||
|
const paperNumberRedux = useAppSelector(
|
||||||
|
(state) => state.state.paperNumberRedux
|
||||||
|
);
|
||||||
|
//supabase
|
||||||
|
const supabase = createClient();
|
||||||
|
|
||||||
function moveReferenceUp(index: number) {
|
function moveReferenceUp(index: number) {
|
||||||
console.log("index", index);
|
console.log("index", index);
|
||||||
|
@ -70,11 +77,16 @@ function ReferenceList({ editor }: ReferenceListProps) {
|
||||||
const handleClearReferences = () => {
|
const handleClearReferences = () => {
|
||||||
dispatch(clearReferencesRedux());
|
dispatch(clearReferencesRedux());
|
||||||
};
|
};
|
||||||
|
//监听references,如果发生变化,就提交到服务器
|
||||||
|
React.useEffect(() => {
|
||||||
|
submitPaper(supabase, undefined, references, paperNumberRedux);
|
||||||
|
}, [references]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto p-4">
|
<div className="container mx-auto p-4">
|
||||||
{/* 表单区域 */}
|
{/* 表单区域 */}
|
||||||
<form
|
<form
|
||||||
onSubmit={(e) => {
|
onSubmit={async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleAddReference({
|
handleAddReference({
|
||||||
title: newTitle,
|
title: newTitle,
|
||||||
|
@ -89,6 +101,7 @@ function ReferenceList({ editor }: ReferenceListProps) {
|
||||||
setNewYear("");
|
setNewYear("");
|
||||||
setNewPublisher("");
|
setNewPublisher("");
|
||||||
setNewUrl("");
|
setNewUrl("");
|
||||||
|
// submitPaper(supabase, undefined, references, paperNumberRedux);
|
||||||
}}
|
}}
|
||||||
className="mb-6"
|
className="mb-6"
|
||||||
>
|
>
|
||||||
|
@ -210,6 +223,7 @@ function ReferenceList({ editor }: ReferenceListProps) {
|
||||||
</button>
|
</button>
|
||||||
<ParagraphDeleteButton
|
<ParagraphDeleteButton
|
||||||
index={index}
|
index={index}
|
||||||
|
isRemovePaper={true}
|
||||||
removeReferenceUpdateIndex={removeReferenceUpdateIndex}
|
removeReferenceUpdateIndex={removeReferenceUpdateIndex}
|
||||||
></ParagraphDeleteButton>
|
></ParagraphDeleteButton>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -10,6 +10,31 @@ import {
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { faArrowLeft } from "@fortawesome/free-solid-svg-icons";
|
import { faArrowLeft } from "@fortawesome/free-solid-svg-icons";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { useLocalStorage } from "react-use";
|
||||||
|
|
||||||
|
// 在 Settings.tsx 或一个单独的配置文件中
|
||||||
|
const CONFIG_OPTIONS = [
|
||||||
|
{
|
||||||
|
name: "cocopilot-gpt4(apiKey在前面手动加上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 = () => {
|
const Settings = () => {
|
||||||
//redux
|
//redux
|
||||||
|
@ -17,7 +42,11 @@ const Settings = () => {
|
||||||
const apiKey = useAppSelector((state) => state.auth.apiKey);
|
const apiKey = useAppSelector((state) => state.auth.apiKey);
|
||||||
const upstreamUrl = useAppSelector((state) => state.auth.upsreamUrl);
|
const upstreamUrl = useAppSelector((state) => state.auth.upsreamUrl);
|
||||||
const systemPrompt = useAppSelector((state) => state.auth.systemPrompt);
|
const systemPrompt = useAppSelector((state) => state.auth.systemPrompt);
|
||||||
|
//state
|
||||||
|
const [userConfigNumber, setUserConfigNumber] = useLocalStorage(
|
||||||
|
"userConfigNumber",
|
||||||
|
"2"
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<div className="max-w-md rounded overflow-hidden shadow-lg bg-blue-gray-100 z-1000 mx-auto ">
|
<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>
|
<h1 className="font-bold text-3xl">settings</h1>
|
||||||
|
@ -27,6 +56,31 @@ const Settings = () => {
|
||||||
<FontAwesomeIcon icon={faArrowLeft} size="2x" />
|
<FontAwesomeIcon icon={faArrowLeft} size="2x" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</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 */}
|
{/* api key */}
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label
|
<label
|
||||||
|
@ -37,7 +91,7 @@ const Settings = () => {
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="api-key"
|
id="api-key"
|
||||||
type="text"
|
type="password"
|
||||||
value={apiKey}
|
value={apiKey}
|
||||||
onChange={(event) => dispatch(setApiKey(event.target.value))}
|
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"
|
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
|
<textarea
|
||||||
id="system-prompt"
|
id="system-prompt"
|
||||||
value={systemPrompt}
|
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"
|
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}
|
rows={8}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -77,7 +77,7 @@ const sendMessageToOpenAI = async (
|
||||||
requestOptions
|
requestOptions
|
||||||
);
|
);
|
||||||
if (!response.ok || !response.body) {
|
if (!response.ok || !response.body) {
|
||||||
throw new Error("Server responded with an error" + response);
|
throw new Error("");
|
||||||
}
|
}
|
||||||
const reader = response.body.getReader();
|
const reader = response.body.getReader();
|
||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder();
|
||||||
|
@ -88,11 +88,11 @@ const sendMessageToOpenAI = async (
|
||||||
convertToSuperscript(editor);
|
convertToSuperscript(editor);
|
||||||
updateBracketNumbersInDeltaKeepSelection(editor);
|
updateBracketNumbersInDeltaKeepSelection(editor);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error:", error);
|
// console.error("Error:", error);
|
||||||
// 如果有响应,返回响应的原始内容
|
// 如果有响应,返回响应的原始内容
|
||||||
if (response) {
|
if (response) {
|
||||||
const rawResponse = await response.text();
|
const rawResponse = await response.text();
|
||||||
throw new Error(`Error: ${error.message}, Response: ${rawResponse}`);
|
throw new Error(`请求发生错误: ${error}, Response: ${rawResponse}`);
|
||||||
}
|
}
|
||||||
// 如果没有响应,只抛出错误
|
// 如果没有响应,只抛出错误
|
||||||
throw error;
|
throw error;
|
||||||
|
@ -166,7 +166,8 @@ async function processResult(reader, decoder, editor) {
|
||||||
// 如果 jsonStr 以 "data: " 开头,就移除这个前缀
|
// 如果 jsonStr 以 "data: " 开头,就移除这个前缀
|
||||||
// 移除字符串首尾的空白字符
|
// 移除字符串首尾的空白字符
|
||||||
jsonStr = jsonStr.trim();
|
jsonStr = jsonStr.trim();
|
||||||
jsonStr = jsonStr.substring(6);
|
// jsonStr = jsonStr.substring(6);
|
||||||
|
jsonStr = jsonStr.replace("data:", "");
|
||||||
let dataObject = JSON.parse(jsonStr);
|
let dataObject = JSON.parse(jsonStr);
|
||||||
// console.log("dataObject", dataObject);
|
// console.log("dataObject", dataObject);
|
||||||
// 处理 dataObject 中的 content
|
// 处理 dataObject 中的 content
|
||||||
|
@ -182,9 +183,16 @@ async function processResult(reader, decoder, editor) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// console.error("Failed to parse JSON object:", jsonStr);
|
// console.error(
|
||||||
console.error("Error:", error);
|
// "there is a error in parse JSON object:",
|
||||||
break;
|
// jsonStr,
|
||||||
|
// "error reason",
|
||||||
|
// error
|
||||||
|
// );
|
||||||
|
// break;
|
||||||
|
throw new Error(`
|
||||||
|
there is a error in parse JSON object: ${jsonStr},
|
||||||
|
error reason: ${error}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
@ -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>
|
|
||||||
|
|
||||||
<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
219
package-lock.json
generated
|
@ -28,10 +28,12 @@
|
||||||
"postcss": "8.4.31",
|
"postcss": "8.4.31",
|
||||||
"quill": "^1.3.7",
|
"quill": "^1.3.7",
|
||||||
"quill-to-word": "^1.3.0",
|
"quill-to-word": "^1.3.0",
|
||||||
|
"raw-body": "^2.5.2",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-quill": "^2.0.0",
|
"react-quill": "^2.0.0",
|
||||||
"react-redux": "^9.1.0",
|
"react-redux": "^9.1.0",
|
||||||
|
"react-transition-group": "^4.4.5",
|
||||||
"react-use": "^17.4.3",
|
"react-use": "^17.4.3",
|
||||||
"redux": "^5.0.1",
|
"redux": "^5.0.1",
|
||||||
"redux-logger": "^3.0.6",
|
"redux-logger": "^3.0.6",
|
||||||
|
@ -50,6 +52,7 @@
|
||||||
"@types/node": "^20.3.1",
|
"@types/node": "^20.3.1",
|
||||||
"@types/react": "^18.2.48",
|
"@types/react": "^18.2.48",
|
||||||
"@types/react-dom": "18.2.5",
|
"@types/react-dom": "18.2.5",
|
||||||
|
"@types/react-transition-group": "^4.4.10",
|
||||||
"@types/redux-logger": "^3.0.12",
|
"@types/redux-logger": "^3.0.12",
|
||||||
"encoding": "^0.1.13"
|
"encoding": "^0.1.13"
|
||||||
}
|
}
|
||||||
|
@ -579,6 +582,15 @@
|
||||||
"@types/react": "*"
|
"@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": {
|
"node_modules/@types/redux-logger": {
|
||||||
"version": "3.0.12",
|
"version": "3.0.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/redux-logger/-/redux-logger-3.0.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/redux-logger/-/redux-logger-3.0.12.tgz",
|
||||||
|
@ -814,6 +826,14 @@
|
||||||
"node": ">=10.16.0"
|
"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": {
|
"node_modules/call-bind": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz",
|
||||||
|
@ -1091,6 +1111,14 @@
|
||||||
"node": ">=0.4.0"
|
"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": {
|
"node_modules/didyoumean": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz",
|
||||||
"integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ=="
|
"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": {
|
"node_modules/eastasianwidth": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||||
|
@ -1522,6 +1559,21 @@
|
||||||
"node": ">= 0.4"
|
"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": {
|
"node_modules/humanize-ms": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
|
||||||
|
@ -2406,6 +2458,31 @@
|
||||||
"url": "https://opencollective.com/ramda"
|
"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": {
|
"node_modules/react": {
|
||||||
"version": "18.2.0",
|
"version": "18.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
|
"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": {
|
"node_modules/react-universal-interface": {
|
||||||
"version": "0.6.2",
|
"version": "0.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-universal-interface/-/react-universal-interface-0.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-universal-interface/-/react-universal-interface-0.6.2.tgz",
|
||||||
|
@ -2664,8 +2756,7 @@
|
||||||
"node_modules/safer-buffer": {
|
"node_modules/safer-buffer": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||||
"devOptional": true
|
|
||||||
},
|
},
|
||||||
"node_modules/sax": {
|
"node_modules/sax": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
|
@ -2739,6 +2830,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||||
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
|
"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": {
|
"node_modules/shebang-command": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||||
|
@ -2892,6 +2988,14 @@
|
||||||
"stacktrace-gps": "^3.0.4"
|
"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": {
|
"node_modules/streamsearch": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
|
||||||
"integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ=="
|
"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": {
|
"node_modules/tr46": {
|
||||||
"version": "0.0.3",
|
"version": "0.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
|
"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": {
|
"node_modules/update-browserslist-db": {
|
||||||
"version": "1.0.13",
|
"version": "1.0.13",
|
||||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
|
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
|
||||||
|
@ -3804,6 +3924,15 @@
|
||||||
"@types/react": "*"
|
"@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": {
|
"@types/redux-logger": {
|
||||||
"version": "3.0.12",
|
"version": "3.0.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/redux-logger/-/redux-logger-3.0.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/redux-logger/-/redux-logger-3.0.12.tgz",
|
||||||
|
@ -3968,6 +4097,11 @@
|
||||||
"streamsearch": "^1.1.0"
|
"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": {
|
"call-bind": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
|
"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": {
|
"didyoumean": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
"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": {
|
"eastasianwidth": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||||
|
@ -4481,6 +4629,18 @@
|
||||||
"function-bind": "^1.1.2"
|
"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": {
|
"humanize-ms": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/ramda/-/ramda-0.29.1.tgz",
|
||||||
"integrity": "sha512-OfxIeWzd4xdUNxlWhgFazxsA/nl3mS4/jGZI5n00uWOoSSFRhC1b6gl6xvmzUamgmqELraWp0J/qqVlXYPDPyA=="
|
"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": {
|
"react": {
|
||||||
"version": "18.2.0",
|
"version": "18.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
|
||||||
|
@ -5116,6 +5297,17 @@
|
||||||
"use-sync-external-store": "^1.0.0"
|
"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": {
|
"react-universal-interface": {
|
||||||
"version": "0.6.2",
|
"version": "0.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-universal-interface/-/react-universal-interface-0.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-universal-interface/-/react-universal-interface-0.6.2.tgz",
|
||||||
|
@ -5262,8 +5454,7 @@
|
||||||
"safer-buffer": {
|
"safer-buffer": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||||
"devOptional": true
|
|
||||||
},
|
},
|
||||||
"sax": {
|
"sax": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
|
@ -5322,6 +5513,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||||
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
|
"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": {
|
"shebang-command": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||||
|
@ -5446,6 +5642,11 @@
|
||||||
"stacktrace-gps": "^3.0.4"
|
"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": {
|
"streamsearch": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
|
||||||
"integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ=="
|
"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": {
|
"tr46": {
|
||||||
"version": "0.0.3",
|
"version": "0.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
|
"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": {
|
"update-browserslist-db": {
|
||||||
"version": "1.0.13",
|
"version": "1.0.13",
|
||||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
|
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
|
||||||
|
|
|
@ -27,10 +27,12 @@
|
||||||
"postcss": "8.4.31",
|
"postcss": "8.4.31",
|
||||||
"quill": "^1.3.7",
|
"quill": "^1.3.7",
|
||||||
"quill-to-word": "^1.3.0",
|
"quill-to-word": "^1.3.0",
|
||||||
|
"raw-body": "^2.5.2",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-quill": "^2.0.0",
|
"react-quill": "^2.0.0",
|
||||||
"react-redux": "^9.1.0",
|
"react-redux": "^9.1.0",
|
||||||
|
"react-transition-group": "^4.4.5",
|
||||||
"react-use": "^17.4.3",
|
"react-use": "^17.4.3",
|
||||||
"redux": "^5.0.1",
|
"redux": "^5.0.1",
|
||||||
"redux-logger": "^3.0.6",
|
"redux-logger": "^3.0.6",
|
||||||
|
@ -49,6 +51,7 @@
|
||||||
"@types/node": "^20.3.1",
|
"@types/node": "^20.3.1",
|
||||||
"@types/react": "^18.2.48",
|
"@types/react": "^18.2.48",
|
||||||
"@types/react-dom": "18.2.5",
|
"@types/react-dom": "18.2.5",
|
||||||
|
"@types/react-transition-group": "^4.4.10",
|
||||||
"@types/redux-logger": "^3.0.12",
|
"@types/redux-logger": "^3.0.12",
|
||||||
"encoding": "^0.1.13"
|
"encoding": "^0.1.13"
|
||||||
},
|
},
|
||||||
|
|
|
@ -16,6 +16,7 @@ module.exports = {
|
||||||
"blue-gray": {
|
"blue-gray": {
|
||||||
100: "#F0F4F8", // 这里使用你选择的颜色值
|
100: "#F0F4F8", // 这里使用你选择的颜色值
|
||||||
},
|
},
|
||||||
|
gold: "#FFD700",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
import { Reference } from "@/utils/global";
|
import { Reference } from "@/utils/global";
|
||||||
import Quill from "quill";
|
import Quill from "quill";
|
||||||
|
import { animated, useSpring } from "@react-spring/web";
|
||||||
|
|
||||||
function getTextBeforeCursor(quill, length = 500) {
|
function getTextBeforeCursor(quill: Quill, length = 500) {
|
||||||
const cursorPosition = quill.getSelection().index;
|
const cursorPosition = quill.getSelection()!.index;
|
||||||
const start = Math.max(0, cursorPosition - length); // 确保开始位置不是负数
|
const start = Math.max(0, cursorPosition - length); // 确保开始位置不是负数
|
||||||
return quill.getText(start, cursorPosition - start);
|
return quill.getText(start, cursorPosition - start);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNumberBeforeCursor(quill, length = 3000) {
|
function getNumberBeforeCursor(quill: Quill, length = 3000) {
|
||||||
const cursorPosition = quill.getSelection().index;
|
const cursorPosition = quill.getSelection()!.index;
|
||||||
const start = Math.max(0, cursorPosition - length); // 确保开始位置不是负数
|
const start = Math.max(0, cursorPosition - length); // 确保开始位置不是负数
|
||||||
const textBeforeCursor = quill.getText(start, cursorPosition - start);
|
const textBeforeCursor = quill.getText(start, cursorPosition - start);
|
||||||
|
|
||||||
|
@ -31,10 +32,10 @@ function getNumberBeforeCursor(quill, length = 3000) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateBracketNumbersInDelta(delta) {
|
function updateBracketNumbersInDelta(delta: any) {
|
||||||
let currentNumber = 1;
|
let currentNumber = 1;
|
||||||
|
|
||||||
const updatedOps = delta.ops.map((op) => {
|
const updatedOps = delta.ops.map((op: any) => {
|
||||||
if (typeof op.insert === "string") {
|
if (typeof op.insert === "string") {
|
||||||
return {
|
return {
|
||||||
...op,
|
...op,
|
||||||
|
@ -48,9 +49,9 @@ function updateBracketNumbersInDelta(delta) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteReferenceNumberOrParagraph(
|
function deleteReferenceNumberOrParagraph(
|
||||||
delta,
|
delta: any,
|
||||||
indexToRemove: number,
|
indexToRemove: number,
|
||||||
quill,
|
quill: Quill,
|
||||||
deleteParagraph: boolean
|
deleteParagraph: boolean
|
||||||
) {
|
) {
|
||||||
const indexStr = `[${indexToRemove + 1}]`;
|
const indexStr = `[${indexToRemove + 1}]`;
|
||||||
|
@ -63,7 +64,7 @@ function deleteReferenceNumberOrParagraph(
|
||||||
let delta = quill.clipboard.convert(htmlString);
|
let delta = quill.clipboard.convert(htmlString);
|
||||||
return delta;
|
return delta;
|
||||||
} else {
|
} else {
|
||||||
const updatedOps = delta.ops.flatMap((op, i) => {
|
const updatedOps = delta.ops.flatMap((op: any, i) => {
|
||||||
if (typeof op.insert === "string") {
|
if (typeof op.insert === "string") {
|
||||||
const indexPos = op.insert.indexOf(indexStr);
|
const indexPos = op.insert.indexOf(indexStr);
|
||||||
if (indexPos !== -1) {
|
if (indexPos !== -1) {
|
||||||
|
@ -131,7 +132,7 @@ function removeParagraphWithReference(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateBracketNumbersInDeltaKeepSelection(quill) {
|
function updateBracketNumbersInDeltaKeepSelection(quill: Quill) {
|
||||||
const selection = quill.getSelection();
|
const selection = quill.getSelection();
|
||||||
const delta = quill.getContents();
|
const delta = quill.getContents();
|
||||||
const updatedDelta = updateBracketNumbersInDelta(delta);
|
const updatedDelta = updateBracketNumbersInDelta(delta);
|
||||||
|
@ -142,7 +143,7 @@ function updateBracketNumbersInDeltaKeepSelection(quill) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function delteIndexUpdateBracketNumbersInDeltaKeepSelection(
|
export function delteIndexUpdateBracketNumbersInDeltaKeepSelection(
|
||||||
quill,
|
quill: Quill,
|
||||||
index: number,
|
index: number,
|
||||||
rmPg: boolean
|
rmPg: boolean
|
||||||
) {
|
) {
|
||||||
|
@ -161,7 +162,7 @@ export function delteIndexUpdateBracketNumbersInDeltaKeepSelection(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertToSuperscript(quill) {
|
function convertToSuperscript(quill: Quill) {
|
||||||
const text = quill.getText();
|
const text = quill.getText();
|
||||||
const regex = /\[\d+\]/g; // 正则表达式匹配 "[数字]" 格式
|
const regex = /\[\d+\]/g; // 正则表达式匹配 "[数字]" 格式
|
||||||
let match;
|
let match;
|
||||||
|
@ -220,7 +221,7 @@ function formatAllReferencesForCopy(references: Reference[]): string {
|
||||||
.join("\n");
|
.join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatTextInEditor(editor) {
|
export function formatTextInEditor(editor: Quill) {
|
||||||
convertToSuperscript(editor);
|
convertToSuperscript(editor);
|
||||||
updateBracketNumbersInDeltaKeepSelection(editor);
|
updateBracketNumbersInDeltaKeepSelection(editor);
|
||||||
}
|
}
|
||||||
|
|
13
utils/supabase/servicerole.ts
Normal file
13
utils/supabase/servicerole.ts
Normal 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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
32
utils/supabase/supabaseaql.sql
Normal file
32
utils/supabase/supabaseaql.sql
Normal 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;
|
197
utils/supabase/supabaseutils back.ts
Normal file
197
utils/supabase/supabaseutils back.ts
Normal 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相关联的论文
|
174
utils/supabase/supabaseutils.ts
Normal file
174
utils/supabase/supabaseutils.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user