├── .env.example
├── .eslintrc.json
├── .gitignore
├── ReadMe.md
├── ai
├── baidu.ts
├── openai.ts
└── vectorstore.ts
├── app
├── (routes)
│ └── dashboard
│ │ ├── [categoryId]
│ │ └── page.tsx
│ │ ├── _components
│ │ ├── category-creator.tsx
│ │ ├── default-table-columns.tsx
│ │ ├── item-actions.tsx
│ │ ├── items-browser.tsx
│ │ ├── items-table.tsx
│ │ ├── note-creator.tsx
│ │ ├── paper-table-columns.tsx
│ │ ├── paper-uploader.tsx
│ │ ├── recent-browser.tsx
│ │ ├── recent-item.tsx
│ │ └── webs-browser.tsx
│ │ └── layout.tsx
├── api
│ ├── category
│ │ ├── [categoryId]
│ │ │ └── route.ts
│ │ └── route.ts
│ ├── chat
│ │ └── route.ts
│ ├── image
│ │ └── route.ts
│ ├── message
│ │ └── route.ts
│ ├── note
│ │ ├── [noteId]
│ │ │ └── route.ts
│ │ └── route.ts
│ ├── paper
│ │ ├── [paperId]
│ │ │ └── route.ts
│ │ └── route.ts
│ └── translate
│ │ └── route.ts
├── favicon.ico
├── globals.css
├── layout.tsx
├── notes
│ └── [noteId]
│ │ └── page.tsx
├── page.tsx
└── papers
│ └── [paperId]
│ ├── _components
│ ├── chat
│ │ ├── chat-client.tsx
│ │ ├── chat-form.tsx
│ │ ├── chat-message.tsx
│ │ └── chat-messages.tsx
│ ├── menu-bar.tsx
│ ├── note-navbar.tsx
│ ├── note-sidebar.tsx
│ ├── paper-chat.tsx
│ ├── paper-info.tsx
│ ├── paper-note.tsx
│ ├── paper-notes.tsx
│ ├── paper-reader.tsx
│ ├── pdf-viewer.tsx
│ ├── style-setter.tsx
│ └── tool-button.tsx
│ └── page.tsx
├── components.json
├── components
├── category-list.tsx
├── default-categories.tsx
├── editors
│ ├── excalidraw-editor.tsx
│ ├── icon-picker.tsx
│ └── markdown-editor.tsx
├── heading.tsx
├── heros.tsx
├── logo.tsx
├── navbar.tsx
├── search-bar.tsx
├── sidebar-item.tsx
├── sidebar.tsx
└── ui
│ ├── alert-dialog.tsx
│ ├── avatar.tsx
│ ├── button.tsx
│ ├── dialog.tsx
│ ├── dropdown-menu.tsx
│ ├── form.tsx
│ ├── input.tsx
│ ├── label.tsx
│ ├── popover.tsx
│ ├── progress.tsx
│ ├── select.tsx
│ ├── separator.tsx
│ ├── skeleton.tsx
│ ├── slider.tsx
│ ├── table.tsx
│ └── tooltip.tsx
├── constants.ts
├── data
├── category.ts
├── message.ts
├── note.ts
└── paper.ts
├── hooks
└── use-annotations.tsx
├── lib
├── prismadb.ts
└── utils.ts
├── next.config.mjs
├── package-lock.json
├── package.json
├── postcss.config.js
├── prisma
├── researcher.db
└── schema.prisma
├── public
├── bot.svg
├── dashboard.png
├── documents-dark.png
├── documents.png
├── empty-dark.png
├── empty.png
├── error.png
├── files.png
├── men-dark.svg
├── men.svg
├── paper-reading.png
├── papers
│ └── 1718771923339.pdf
├── reading-dark.png
├── reading.png
└── user.svg
├── tailwind.config.ts
├── tsconfig.json
└── types
└── types.tsx
/.env.example:
--------------------------------------------------------------------------------
1 | # Environment variables declared in this file are automatically made available to Prisma.
2 | # See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
3 |
4 | # Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
5 | # See the documentation for all the connection string options: https://pris.ly/d/connection-strings
6 |
7 | DATABASE_URL="file:./researcher.db"
8 |
9 | BAIDU_TRANSLATION_APPID=
10 | BAIDU_TRANSLATION_SECRET_KEY=
11 |
12 | OPENAI_API_KEY=
13 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # public files
24 | /public/images
25 | /public/papers
26 |
27 | /prisma/researcher.db
28 |
29 | # debug
30 | npm-debug.log*
31 | yarn-debug.log*
32 | yarn-error.log*
33 |
34 | # local env files
35 | .env*.local
36 | .env
37 |
38 | logs
39 |
40 | # vercel
41 | .vercel
42 |
43 | # typescript
44 | *.tsbuildinfo
45 | next-env.d.ts
46 |
47 | nohup.out
--------------------------------------------------------------------------------
/ReadMe.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Knowtate
4 |
5 |
6 |
7 |
8 | Empowering Research Excellence
9 | Knowtate is a scientific research assistant app offering a suite of features for literature reading, management, and intelligent QA with AI. Embrace the power of centralized document control, seamless reading, and intelligent QA with AI of your document—all within a single, streamlined application.
10 |
11 |
12 |
13 |
14 |
15 |
16 | > [!NOTE]
17 | > You can try Knowtate online at: https://knowtate.online or deploy it on your own local enviroment by following the instructions of [Quick Start Part](#quick-start).
18 |
19 | ## Features
20 |
21 | ### Literature Management and Reading
22 | Upload and manage your research papers with a central repository backed by object storage service. This service can be self-hostable or various cloud storage solutions like AWS S3. Enjoy an online PDF reader that enhances your literature reading experience.
23 |
24 |
25 |
26 |
27 |
28 |
29 | ### Markdown Editor:
30 | For the markdown aficionados, the Markdown Editor provides a straightforward, text-focused interface. This allows for efficient writing and formatting with the simplicity and power of markdown syntax—perfect for those who prefer keyboard-centric controls and clean, exportable content.
31 |
32 | ### Intelligent Paper Q&A:
33 | Powered by a RAG-based intelligent Q&A system, Knowtate allows you to inquire about any aspect of your papers. Customize your dialogue models, vectorization models, and vector databases to suit your needs. Currently supporting OpenAI ChatGPT and OpenAI Embeddings for insightful interactions.
34 |
35 | ### More Features on the Horizon:
36 | Development is ongoing for additional features to enhance your research experience even further.
37 |
38 | ## Quick Start
39 |
40 | ### Run Locally
41 |
42 | #### 1. Clone the Repository Locally
43 | ```bash
44 | # Navigate to your desired path
45 | cd /your/path/
46 | git clone https://github.com/tsmotlp/knowtate.git
47 | ```
48 |
49 | #### 2. Configure Environment Variables
50 | ```bash
51 | cd /your/path/knowtate
52 | cp .env.example .env
53 | # Change `.env.example` to `.env` and update the following variables in `.env`:
54 |
55 | # Deployment used by `npx convex dev`
56 | DATABASE_URL="file:./researcher.db"
57 |
58 | BAIDU_TRANSLATION_APPID=
59 | BAIDU_TRANSLATION_SECRET_KEY=
60 |
61 | OPENAI_API_KEY=
62 | PROXY_URL=
63 |
64 | ```
65 |
66 | > For Windows users, find your network proxy address in Settings -> Network and Internet -> Manual proxy setup -> Edit. Enter the server information in the format http://localhost:10077 as your PROXY_URL.
67 |
68 | > For Linux users, determine your network proxy address with:
69 |
70 | ```bash
71 | echo $HTTP_PROXY
72 | ```
73 |
74 | #### 4. Install Depedencies:
75 | ```bash
76 | > cd path/to/knowtate
77 | > npm install # or pnpm install or yarn
78 | ```
79 |
80 | #### 5. Run Knowtate:
81 | ```bash
82 | > cd path/to/knowtate
83 | > npm run dev # or pnpm run dev or yarn dev
84 | ```
85 | Access Knowtate at http://localhost:3000 and delve into your personalized academic research experience.
86 |
87 | ## Copyright and License
88 | The contents of this repository are licensed under the [MIT License](https://github.com/tsmotlp/knowtate/blob/master/LICENSE). We respect and adhere to the copyright and usage permissions of each paper and resource.
89 |
90 | ## Contact Information
91 | For any questions or suggestions, please contact us through the following:
92 |
93 | - [Submit an Issue](https://github.com/tsmotlp/knowtate/issues)
94 | - Email: yqhuang2912@gmail.com
--------------------------------------------------------------------------------
/ai/baidu.ts:
--------------------------------------------------------------------------------
1 | import OpenAI from "openai";
2 | import { FaissStore } from "@langchain/community/vectorstores/faiss";
3 | import { OpenAIEmbeddings } from "@langchain/openai";
4 | import { OpenAIStream, StreamingTextResponse } from "ai";
5 | import { DATA_DIR, HISTORY_MESSAGE_N, VECTOR_SEARCH_K } from "@/constants";
6 | import { HttpsProxyAgent } from "https-proxy-agent";
7 | import { ChatMessageProps } from "@/app/papers/[paperId]/_components/chat/chat-message";
8 | import axios from "axios";
9 |
10 |
11 | export async function BaiduChat(paperId: string, prompt: string, prevMessages: ChatMessageProps[] | undefined) {
12 | try {
13 | // // 1. vectorize use message
14 | // const embeddings = new OpenAIEmbeddings({
15 | // openAIApiKey: process.env.OPENAI_API_KEY,
16 | // configuration: {
17 | // httpAgent: proxyAgent,
18 | // },
19 | // });
20 |
21 | // const vectors_path = `${DATA_DIR}/faiss/${paperId}`;
22 |
23 | // const vectorstore = await FaissStore.load(vectors_path, embeddings);
24 |
25 | // const results = await vectorstore.similaritySearch(prompt, VECTOR_SEARCH_K);
26 |
27 | const formattedPrevMessages = prevMessages ? prevMessages.map((msg) => ({
28 | role: msg.role,
29 | content: msg.content,
30 | })) : [];
31 |
32 |
33 | const response = await axios.post(`https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/completions_pro?access_token=${process.env.BAIDU_ACCESS_TOKEN}`, {
34 | messages: [
35 | // {
36 | // role: "system",
37 | // content:
38 | // "Use the following pieces of context (or previous conversaton if needed) to answer the users question in markdown format.",
39 | // },
40 | {
41 | role: "user",
42 | content: `Use the following pieces of context (or previous conversaton if needed) to answer the users question in markdown format. \nIf you don't know the answer, just say that you don't know, don't try to make up an answer.
43 |
44 | \n----------------\n
45 |
46 | PREVIOUS CONVERSATION:
47 | ${formattedPrevMessages.map((message) => {
48 | if (message.role === "user") return `User: ${message.content}\n`;
49 | return `Assistant: ${message.content}\n`;
50 | })}
51 |
52 | \n----------------\n
53 |
54 | USER INPUT: ${prompt}`,
55 | },
56 | ],
57 | stream: true
58 | })
59 | console.log("response", response.data)
60 |
61 | const stream = OpenAIStream(response.data);
62 | return stream
63 | } catch (error) {
64 | console.log("[OPENAI_CHAT_COMPLETION_ERROR]", error)
65 | }
66 | }
--------------------------------------------------------------------------------
/ai/openai.ts:
--------------------------------------------------------------------------------
1 | import OpenAI from "openai";
2 | import { FaissStore } from "@langchain/community/vectorstores/faiss";
3 | import { OpenAIEmbeddings } from "@langchain/openai";
4 | import { OpenAIStream, StreamingTextResponse } from "ai";
5 | import { DATA_DIR, HISTORY_MESSAGE_N, VECTOR_SEARCH_K } from "@/constants";
6 | import { HttpsProxyAgent } from "https-proxy-agent";
7 | import { ChatMessageProps } from "@/app/papers/[paperId]/_components/chat/chat-message";
8 |
9 | const openai = new OpenAI({
10 | apiKey: process.env.OPENAI_API_KEY,
11 | baseURL: "https://api.nextapi.fun/v1"
12 | });
13 |
14 | export async function OpenAIChat(paperId: string, prompt: string, prevMessages: ChatMessageProps[] | undefined) {
15 | try {
16 | // let proxy_url = ""
17 | // if (process.env.PROXY_URL) {
18 | // proxy_url = process.env.PROXY_URL
19 | // }
20 | // const proxyAgent = new HttpsProxyAgent(proxy_url);
21 |
22 | // // 1. vectorize use message
23 | // const embeddings = new OpenAIEmbeddings({
24 | // openAIApiKey: process.env.OPENAI_API_KEY,
25 | // configuration: {
26 | // httpAgent: proxyAgent,
27 | // },
28 | // });
29 |
30 | // const vectors_path = `${DATA_DIR}/faiss/${paperId}`;
31 |
32 | // const vectorstore = await FaissStore.load(vectors_path, embeddings);
33 |
34 | // const results = await vectorstore.similaritySearch(prompt, VECTOR_SEARCH_K);
35 |
36 | const formattedPrevMessages = prevMessages ? prevMessages.map((msg) => ({
37 | role: msg.role,
38 | content: msg.content,
39 | })) : [];
40 |
41 |
42 | const response = await openai.chat.completions.create(
43 | {
44 | model: "gpt-3.5-turbo",
45 | temperature: 0,
46 | stream: true,
47 | messages: [
48 | {
49 | role: "system",
50 | content:
51 | "Use the following pieces of context (or previous conversaton if needed) to answer the users question in markdown format.",
52 | },
53 | {
54 | role: "user",
55 | content: `Use the following pieces of context (or previous conversaton if needed) to answer the users question in markdown format. \nIf you don't know the answer, just say that you don't know, don't try to make up an answer.
56 |
57 | \n----------------\n
58 |
59 | PREVIOUS CONVERSATION:
60 | ${formattedPrevMessages.map((message) => {
61 | if (message.role === "user") return `User: ${message.content}\n`;
62 | return `Assistant: ${message.content}\n`;
63 | })}
64 |
65 | \n----------------\n
66 |
67 |
68 |
69 | USER INPUT: ${prompt}`,
70 | },
71 | ],
72 | },
73 | // {
74 | // httpAgent: proxyAgent,
75 | // timeout: 30000,
76 | // },
77 | );
78 |
79 | const stream = OpenAIStream(response);
80 | return stream
81 | } catch (error) {
82 | console.log("[OPENAI_CHAT_COMPLETION_ERROR]", error)
83 | }
84 | }
--------------------------------------------------------------------------------
/ai/vectorstore.ts:
--------------------------------------------------------------------------------
1 | import { PDFLoader } from "langchain/document_loaders/fs/pdf";
2 | import { FaissStore } from "@langchain/community/vectorstores/faiss";
3 | import { OpenAIEmbeddings } from "@langchain/openai";
4 | import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
5 | import { HttpsProxyAgent } from "https-proxy-agent";
6 | import { DATA_DIR } from "@/constants";
7 | import * as fs from 'fs';
8 | import path from "path";
9 |
10 | export const saveFileToVectorstore = async (paperId: string, paperUrl: string) => {
11 | try {
12 | const save_path = `${DATA_DIR}/faiss/${paperId}`;
13 | if (fs.existsSync(save_path)) {
14 | return
15 | }
16 | const paperPath = path.join(process.cwd(), "public", paperUrl)
17 | const loader = new PDFLoader(paperPath);
18 | const documents = await loader.load();
19 | const splitter = new RecursiveCharacterTextSplitter({
20 | chunkSize: 1000,
21 | chunkOverlap: 100,
22 | });
23 |
24 | const chunks = await splitter.splitDocuments(documents);
25 |
26 | const proxyAgent = new HttpsProxyAgent(process.env.PROXY_URL!);
27 |
28 | const embeddings = new OpenAIEmbeddings({
29 | openAIApiKey: process.env.OPENAP_API_KEY,
30 | configuration: {
31 | httpAgent: proxyAgent,
32 | },
33 | });
34 |
35 | const vectors = await FaissStore.fromDocuments(chunks, embeddings);
36 |
37 | await vectors.save(save_path);
38 | } catch (error) {
39 | throw new Error(`Embedding file to faiss got error: ${error}`);
40 | }
41 | };
42 |
--------------------------------------------------------------------------------
/app/(routes)/dashboard/[categoryId]/page.tsx:
--------------------------------------------------------------------------------
1 | import { getArchivedCategories, getCategoriesByParent, getCategory, getFavoritedCategories } from "@/data/category"
2 | import { getArchivedNotesWithPaper, getFavoritedNotesWithPaper, getNotesWithPaper } from "@/data/note"
3 | import { getArchivedPapers, getFavoritedPapers, getPapers, getRecentPapers } from "@/data/paper"
4 | import { ItemsBrowser } from "../_components/items-browser"
5 | import { DashboardItem, DashboardItemType, NoteWithPaper } from "@/types/types"
6 | import { Category, Paper } from "@prisma/client"
7 | import { RecentBrowser } from "../_components/recent-browser"
8 | import { WebsBrowser } from "../_components/webs-browser"
9 |
10 | interface CategoryIdPageProps {
11 | params: {
12 | categoryId: string
13 | }
14 | }
15 |
16 | const CategoryIdPage = async ({ params }: CategoryIdPageProps) => {
17 | const category = await getCategory(params.categoryId)
18 | let subCategories: Category[] | undefined = []
19 | let papers: Paper[] | undefined = []
20 | let notes: NoteWithPaper[] | undefined = []
21 | if (params.categoryId === "recents") {
22 | papers = await getRecentPapers()
23 | } else if (params.categoryId === "trash") {
24 | subCategories = await getArchivedCategories()
25 | notes = await getArchivedNotesWithPaper()
26 | papers = await getArchivedPapers()
27 | } else if (params.categoryId === "favorites") {
28 | subCategories = await getFavoritedCategories()
29 | notes = await getFavoritedNotesWithPaper()
30 | papers = await getFavoritedPapers()
31 | } else {
32 | subCategories = await getCategoriesByParent(params.categoryId)
33 | notes = await getNotesWithPaper(params.categoryId)
34 | papers = await getPapers(params.categoryId)
35 | }
36 |
37 | const items: DashboardItem[] = []
38 | if (subCategories) {
39 | subCategories.map((c) => (items.push({
40 | id: c.id,
41 | label: c.name,
42 | type: DashboardItemType.Category,
43 | archived: c.archived,
44 | favorited: c.favorited,
45 | url: null,
46 | authors: null,
47 | publication: null,
48 | publicationDate: null,
49 | paperTile: null,
50 | paperId: null,
51 | lastEdit: c.updatedAt,
52 | })))
53 | }
54 |
55 | if (papers) {
56 | papers.map((p) => (items.push({
57 | id: p.id,
58 | label: p.title,
59 | type: DashboardItemType.Paper,
60 | archived: p.archived,
61 | favorited: p.favorited,
62 | url: p.url,
63 | authors: p.authors,
64 | publication: p.publication,
65 | publicationDate: p.publicateDate,
66 | paperTile: null,
67 | paperId: null,
68 | lastEdit: p.updatedAt,
69 | })))
70 | }
71 |
72 | if (notes) {
73 | notes.map((n) => (items.push({
74 | id: n.id,
75 | label: n.title,
76 | type: n.type as DashboardItemType,
77 | archived: n.archived,
78 | favorited: n.favorited,
79 | url: null,
80 | authors: null,
81 | publication: null,
82 | publicationDate: null,
83 | paperTile: n.paper ? n.paper.title : null,
84 | paperId: n.paperId,
85 | lastEdit: n.updatedAt,
86 | })))
87 | }
88 |
89 |
90 | return (
91 |
92 | {params.categoryId === "webs" ? (
93 |
94 | ) : params.categoryId === "recents" ? (
95 |
100 | ) : (
101 |
106 | )}
107 |
108 | );
109 |
110 | }
111 |
112 |
113 | export default CategoryIdPage
--------------------------------------------------------------------------------
/app/(routes)/dashboard/_components/category-creator.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { Button } from "@/components/ui/button"
4 | import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
5 | import { Input } from "@/components/ui/input"
6 | import { CategoryType, DashboardItem, DashboardItemType } from "@/types/types"
7 | import { Category } from "@prisma/client"
8 | import axios from "axios"
9 | import { FolderPlus } from "lucide-react"
10 | import { useRouter } from "next/navigation"
11 | import { Dispatch, SetStateAction, useState } from "react"
12 | import { toast } from "sonner"
13 |
14 | interface CategoryCreatorProps {
15 | type: CategoryType
16 | parentId: string
17 | setItems: Dispatch>
18 | }
19 |
20 | export const CategoryCreator = ({
21 | type,
22 | parentId,
23 | setItems,
24 | }: CategoryCreatorProps) => {
25 | const router = useRouter()
26 | const [categoryName, setCategoryName] = useState("")
27 | const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false)
28 | const handleInputChange = (e: React.ChangeEvent) => {
29 | setCategoryName(e.target.value)
30 | }
31 | const handleCreateCategory = async (name: string) => {
32 | try {
33 | const response = await axios.post("/api/category", JSON.stringify({
34 | name: name,
35 | type: type,
36 | parentId: parentId
37 | }))
38 | if (response.status === 200) {
39 | const createdCategory: Category = response.data
40 | setItems((current) => [...current, {
41 | id: createdCategory.id,
42 | label: createdCategory.name,
43 | type: DashboardItemType.Category,
44 | archived: createdCategory.archived,
45 | favorited: createdCategory.favorited,
46 | url: null,
47 | authors: null,
48 | publication: null,
49 | publicationDate: null,
50 | paperTile: null,
51 | paperId: null,
52 | lastEdit: createdCategory.updatedAt,
53 | }])
54 | toast.success("Category created!");
55 | router.refresh()
56 | }
57 | } catch (error) {
58 | console.log("Failed to create category", error);
59 | toast.error("Failed to create category!");
60 | }
61 | };
62 |
63 | return (
64 | {
67 | setIsCreateDialogOpen(isOpen)
68 | }}
69 | >
70 |
71 |
75 |
76 | 新建分类
77 |
78 |
79 |
80 |
81 | 创建新分类
82 |
83 |
84 |
88 |
89 |
90 | {
93 | handleCreateCategory(categoryName)
94 | setIsCreateDialogOpen(false)
95 | router.refresh()
96 | }}
97 | variant="secondary"
98 | size="sm"
99 | >
100 | 确定
101 |
102 | {
105 | setIsCreateDialogOpen(false)
106 | }}
107 | variant="secondary"
108 | size="sm"
109 | >
110 | 取消
111 |
112 |
113 |
114 |
115 | )
116 | }
--------------------------------------------------------------------------------
/app/(routes)/dashboard/_components/default-table-columns.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { Paper } from "@prisma/client";
4 | import { ColumnDef } from "@tanstack/react-table";
5 | import { format } from "date-fns";
6 | // import { FileActions } from "./file-actions";
7 | import Link from "next/link";
8 | import { BsFiletypePdf, BsMarkdown } from "react-icons/bs"
9 | // import { PaperActions } from "./paper-actions";
10 | import { Folder, PenTool } from "lucide-react";
11 | // import { CategoryActions } from "../../../../../components/category/category-actions";
12 | import { DashboardItem, DashboardItemType } from "@/types/types";
13 | import { ItemActions } from "./item-actions";
14 |
15 |
16 | export const DefaultTableColumns: ColumnDef[] = [
17 | {
18 | header: "标题",
19 | cell: ({ row }) => {
20 | return (
21 |
26 |
27 | {row.original.type === DashboardItemType.Paper && (
28 |
29 | )}
30 | {row.original.type === DashboardItemType.Category && (
31 |
32 | )}
33 | {row.original.type === DashboardItemType.Markdown && (
34 |
35 | )}
36 | {row.original.type === DashboardItemType.Whiteboard && (
37 |
38 | )}
39 |
{row.original.label}
40 |
41 |
42 | )
43 | }
44 | },
45 | {
46 | header: "所属论文",
47 | cell: ({ row }) => {
48 | return (
49 | <>
50 | {row.original.paperId && (
51 |
55 |
56 | {row.original.paperTile}
57 |
58 | )}
59 | >
60 | )
61 | }
62 | },
63 | {
64 | header: "最后修改",
65 | cell: ({ row }) => {
66 | return (
67 |
68 | {format(new Date(row.original.lastEdit), "yyyy/MM/dd")}
69 |
70 | );
71 | },
72 | },
73 | {
74 | header: "管理",
75 | cell: ({ row }) => {
76 | return (
77 |
78 |
81 |
82 | );
83 | },
84 | },
85 | ]
--------------------------------------------------------------------------------
/app/(routes)/dashboard/_components/items-browser.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { defaultCategories } from "@/components/default-categories"
4 | import { SearchBar } from "@/components/search-bar"
5 | import { CategoryType, DashboardItem, DashboardItemType } from "@/types/types"
6 | import { Category } from "@prisma/client"
7 | import { useEffect, useState } from "react"
8 | import { CategoryCreator } from "./category-creator"
9 | import { PaperUploader } from "./paper-uploader"
10 | import { Separator } from "@/components/ui/separator"
11 | import { NoteCreator } from "./note-creator"
12 | import { ItemsTable } from "./items-table"
13 |
14 | interface ItemsBrowserProps {
15 | category: Category | undefined | null,
16 | parentId: string,
17 | initItems: DashboardItem[]
18 | }
19 |
20 | // 这里的item包含Category, Paper和Note
21 | export const ItemsBrowser = ({
22 | category,
23 | parentId,
24 | initItems,
25 | }: ItemsBrowserProps) => {
26 | if (!category) {
27 | category = defaultCategories.find((c) => c.id === parentId)
28 | }
29 | const [query, setQuery] = useState("");
30 | const [items, setItems] = useState(initItems)
31 |
32 |
33 | useEffect(() => {
34 | const filterByQuery = (item: DashboardItem) => item.label.toLowerCase().includes(query.toLowerCase());
35 | setItems(items.filter(filterByQuery));
36 | }, [query])
37 |
38 | return (
39 | <>
40 | {category ? (
41 |
42 |
43 |
44 |
45 | {category.name}
46 |
47 | {category.type === CategoryType.Papers && (
48 |
52 | )}
53 | {category.type === CategoryType.Markdowns && (
54 |
55 |
56 |
57 |
58 | )}
59 | {category.type === CategoryType.Whiteboards && (
60 |
61 |
62 |
63 |
64 | )}
65 |
66 |
67 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | ) : (
79 |
80 | 出错啦
81 |
82 | )}
83 | >
84 | )
85 | }
--------------------------------------------------------------------------------
/app/(routes)/dashboard/_components/items-table.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | flexRender,
5 | getCoreRowModel,
6 | Table as ReactTable,
7 | useReactTable,
8 | } from "@tanstack/react-table";
9 |
10 | import {
11 | Table,
12 | TableBody,
13 | TableCell,
14 | TableHead,
15 | TableHeader,
16 | TableRow,
17 | } from "@/components/ui/table";
18 | import { DefaultTableColumns } from "./default-table-columns";
19 | import Image from "next/image";
20 | import { CategoryType, DashboardItem, DashboardItemType } from "@/types/types";
21 | import { PaperTableColumns } from "./paper-table-columns";
22 |
23 | interface ItemsTableProps {
24 | type: CategoryType,
25 | items: DashboardItem[],
26 | }
27 |
28 | export const ItemsTable = ({
29 | type,
30 | items
31 | }: ItemsTableProps) => {
32 | const columns = type === CategoryType.Papers ? PaperTableColumns : DefaultTableColumns;
33 |
34 | const table = useReactTable({
35 | columns: columns,
36 | data: items,
37 | getCoreRowModel: getCoreRowModel()
38 | });
39 |
40 | return (
41 | <>
42 | {items.length > 0 ? (
43 |
44 |
45 |
46 | {table.getHeaderGroups().map((headerGroup) => (
47 |
48 | {headerGroup.headers.map((header) => {
49 | return (
50 |
53 | {header.isPlaceholder ? null : flexRender(
54 | header.column.columnDef.header,
55 | header.getContext()
56 | )}
57 |
58 | )
59 | })}
60 |
61 | ))}
62 |
63 |
64 | {table.getRowModel().rows?.length > 0 && (
65 | table.getRowModel().rows
66 | .sort(
67 | (a, b) => {
68 | // 首先比较url,空字符串视为较小
69 | if (a.original.type === DashboardItemType.Category && b.original.type !== DashboardItemType.Category) return -1;
70 | if (a.original.type !== DashboardItemType.Category && b.original.type === DashboardItemType.Category) return 1;
71 |
72 | // 如果url相同(都为空或都不为空),则比较updatedAt
73 | return new Date(b.original.lastEdit).getTime() - new Date(a.original.lastEdit).getTime();
74 | }
75 | )
76 | .map((row) => (
77 |
81 | {row.getVisibleCells().map((cell) => (
82 |
83 | {flexRender(cell.column.columnDef.cell, cell.getContext())}
84 |
85 | ))}
86 |
87 | ))
88 | )}
89 |
90 |
91 |
92 | ) : (
93 |
94 |
101 |
108 |
Nothing Found!
109 |
110 | )}
111 | >
112 | )
113 | }
--------------------------------------------------------------------------------
/app/(routes)/dashboard/_components/note-creator.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { Button } from "@/components/ui/button"
4 | import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
5 | import { Input } from "@/components/ui/input"
6 | import { CategoryType, DashboardItem, DashboardItemType } from "@/types/types"
7 | import { Note } from "@prisma/client"
8 | import axios from "axios"
9 | import { FolderPlus } from "lucide-react"
10 | import { useRouter } from "next/navigation"
11 | import { Dispatch, SetStateAction, useState } from "react"
12 | import { toast } from "sonner"
13 |
14 | interface NoteCreatorProps {
15 | type: DashboardItemType
16 | categoryId: string
17 | paperId?: string,
18 | setItems: Dispatch>
19 | }
20 |
21 | export const NoteCreator = ({
22 | type,
23 | categoryId,
24 | paperId,
25 | setItems,
26 | }: NoteCreatorProps) => {
27 | const router = useRouter()
28 | const [noteName, setNoteName] = useState("")
29 | const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false)
30 | const handleInputChange = (e: React.ChangeEvent) => {
31 | setNoteName(e.target.value)
32 | }
33 | const handleCreateNote = async (title: string) => {
34 | try {
35 | const response = await axios.post("/api/note", JSON.stringify({
36 | title: title ? title : "Untitled",
37 | type: type,
38 | categoryId: categoryId,
39 | paperId: paperId,
40 | }))
41 | if (response.status === 200) {
42 | const createdNote: Note = response.data
43 | setItems((current) => [...current, {
44 | id: createdNote.id,
45 | label: createdNote.title,
46 | type: createdNote.type as DashboardItemType,
47 | archived: createdNote.archived,
48 | favorited: createdNote.favorited,
49 | url: null,
50 | authors: null,
51 | publication: null,
52 | publicationDate: null,
53 | paperTile: null,
54 | paperId: createdNote.paperId,
55 | lastEdit: createdNote.updatedAt,
56 | }])
57 | toast.success("Note created!");
58 | router.refresh()
59 | }
60 | } catch (error) {
61 | console.log("Failed to create note", error);
62 | toast.error("Failed to create note!");
63 | }
64 | };
65 |
66 | return (
67 | {
70 | setIsCreateDialogOpen(isOpen)
71 | }}
72 | >
73 |
74 |
78 |
79 | {type === DashboardItemType.Markdown ? "新建笔记" : "新建白板"}
80 |
81 |
82 |
83 |
84 |
85 | {type === DashboardItemType.Markdown ? "创建笔记" : "创建白板"}
86 |
87 |
88 |
89 |
93 |
94 |
95 | {
98 | handleCreateNote(noteName)
99 | setNoteName("")
100 | setIsCreateDialogOpen(false)
101 | router.refresh()
102 | }}
103 | variant="secondary"
104 | size="sm"
105 | >
106 | 确定
107 |
108 | {
110 | setIsCreateDialogOpen(false)
111 | }}
112 | variant="secondary"
113 | size="sm"
114 | >
115 | 取消
116 |
117 |
118 |
119 |
120 | )
121 | }
--------------------------------------------------------------------------------
/app/(routes)/dashboard/_components/paper-table-columns.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { Paper } from "@prisma/client";
4 | import { ColumnDef } from "@tanstack/react-table";
5 | import { format } from "date-fns";
6 | // import { FileActions } from "./file-actions";
7 | import Link from "next/link";
8 | import { BsFiletypePdf, BsMarkdown } from "react-icons/bs"
9 | // import { PaperActions } from "./paper-actions";
10 | import { Folder, PenTool } from "lucide-react";
11 | // import { CategoryActions } from "../../../../../components/category/category-actions";
12 | import { DashboardItem, DashboardItemType } from "@/types/types";
13 | import { ItemActions } from "./item-actions";
14 |
15 |
16 | export const PaperTableColumns: ColumnDef[] = [
17 | {
18 | header: "标题",
19 | cell: ({ row }) => {
20 | return (
21 |
26 |
27 | {row.original.type === DashboardItemType.Paper && (
28 |
29 | )}
30 | {row.original.type === DashboardItemType.Category && (
31 |
32 | )}
33 | {row.original.label}
34 |
35 |
36 | )
37 | }
38 | },
39 | {
40 | header: "作者",
41 | cell: ({ row }) => {
42 | return (
43 |
44 | {row.original.authors}
45 |
46 | )
47 | }
48 | },
49 | {
50 | header: "期刊",
51 | cell: ({ row }) => {
52 | return (
53 |
54 | {row.original.publication}
55 |
56 | )
57 | }
58 | },
59 | // {
60 | // header: "发表时间",
61 | // cell: ({ row }) => {
62 | // return (
63 | //
64 | // {row.original.publicationDate}
65 | //
66 | // )
67 | // }
68 | // },
69 | {
70 | header: "最后修改",
71 | cell: ({ row }) => {
72 | return (
73 |
74 | {format(new Date(row.original.lastEdit), "yyyy/MM/dd")}
75 |
76 | );
77 | },
78 | },
79 | {
80 | header: "管理",
81 | cell: ({ row }) => {
82 | return (
83 |
84 |
87 |
88 | );
89 | },
90 | },
91 | ]
--------------------------------------------------------------------------------
/app/(routes)/dashboard/_components/recent-browser.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { defaultCategories } from "@/components/default-categories"
4 | import { SearchBar } from "@/components/search-bar"
5 | import { CategoryType, DashboardItem, DashboardItemType } from "@/types/types"
6 | import { Category } from "@prisma/client"
7 | import { useEffect, useState } from "react"
8 | import { CategoryCreator } from "./category-creator"
9 | import { PaperUploader } from "./paper-uploader"
10 | import { Separator } from "@/components/ui/separator"
11 | import { NoteCreator } from "./note-creator"
12 | import { RecentItem } from "./recent-item"
13 |
14 | interface RecentBrowserProps {
15 | category: Category | undefined | null,
16 | parentId: string,
17 | initItems: DashboardItem[]
18 | }
19 |
20 | function getStartOfToday(): Date {
21 | const today = new Date();
22 | today.setHours(0, 0, 0, 0);
23 | return today;
24 | }
25 |
26 | function getStartOfYesterday(): Date {
27 | const yesterday = new Date();
28 | yesterday.setDate(yesterday.getDate() - 1);
29 | yesterday.setHours(0, 0, 0, 0);
30 | return yesterday;
31 | }
32 |
33 | function getStartOfLastWeek(): Date {
34 | const today = new Date();
35 | today.setDate(today.getDate() - 7); // 往前推7天
36 | today.setHours(0, 0, 0, 0);
37 | return today;
38 | }
39 |
40 | function getStartOfLastMonth(): Date {
41 | const today = new Date();
42 | today.setDate(today.getDate() - 30); // 往前推30天
43 | today.setHours(0, 0, 0, 0);
44 | return today;
45 | }
46 |
47 | // 这里的item包含Category, Paper和Note
48 | export const RecentBrowser = ({
49 | category,
50 | parentId,
51 | initItems,
52 | }: RecentBrowserProps) => {
53 | if (!category) {
54 | category = defaultCategories.find((c) => c.id === parentId)
55 | }
56 | const [query, setQuery] = useState("");
57 | const [items, setItems] = useState(initItems)
58 | const [expanded, setExpanded] = useState>({});
59 | const onExpand = (id: string) => {
60 | setExpanded(prevExpanded => ({
61 | ...prevExpanded,
62 | [id]: !prevExpanded[id]
63 | }));
64 | };
65 |
66 | const startOfToday = getStartOfToday();
67 | const startOfYesterday = getStartOfYesterday();
68 | const startOfLastWeek = getStartOfLastWeek();
69 | const startOfLastMonth = getStartOfLastMonth();
70 | const today = items.filter(item => item.lastEdit >= startOfToday);
71 | const yesterday = items.filter(item => item.lastEdit >= startOfYesterday && item.lastEdit < startOfToday)
72 | const week = items.filter(item => item.lastEdit >= startOfLastWeek && item.lastEdit < startOfYesterday)
73 | const month = items.filter(item => item.lastEdit >= startOfLastMonth && item.lastEdit < startOfLastWeek)
74 |
75 | useEffect(() => {
76 | const filterByQuery = (item: DashboardItem) => item.label.toLowerCase().includes(query.toLowerCase());
77 | setItems(items.filter(filterByQuery));
78 | }, [query])
79 |
80 | return (
81 | <>
82 | {category ? (
83 |
84 |
85 |
86 |
87 | {category.name}
88 |
89 | {category.type === CategoryType.Papers && (
90 |
94 | )}
95 | {category.type === CategoryType.Markdowns && (
96 |
97 |
98 |
99 |
100 | )}
101 | {category.type === CategoryType.Whiteboards && (
102 |
103 |
104 |
105 |
106 | )}
107 |
108 |
109 |
113 |
114 |
115 |
116 |
117 | onExpand("today")}
121 | items={today}
122 | />
123 | onExpand("yesterday")}
127 | items={yesterday}
128 | />
129 | onExpand("week")}
133 | items={week}
134 | />
135 | onExpand("month")}
139 | items={month}
140 | />
141 |
142 |
143 | ) : (
144 |
145 | 出错啦
146 |
147 | )}
148 | >
149 | )
150 | }
--------------------------------------------------------------------------------
/app/(routes)/dashboard/_components/recent-item.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { DashboardItem } from "@/types/types";
4 | import { format } from "date-fns";
5 | import { ChevronDown, ChevronRight } from "lucide-react";
6 | import Link from "next/link";
7 | import { BsFiletypePdf } from "react-icons/bs";
8 |
9 | interface RecentItemProps {
10 | label: string,
11 | expanded?: boolean
12 | onExpand: () => void
13 | items: DashboardItem[]
14 | }
15 |
16 | export const RecentItem = ({
17 | label,
18 | expanded = true,
19 | onExpand,
20 | items
21 | }: RecentItemProps) => {
22 |
23 | const handleExpand = (
24 | event: React.MouseEvent
25 | ) => {
26 | event.stopPropagation();
27 | onExpand?.();
28 | };
29 |
30 | const ChevronIcon = expanded ? ChevronDown : ChevronRight;
31 |
32 |
33 | return (
34 |
35 |
36 |
41 |
44 |
45 |
46 | {label}
47 |
48 |
49 | {expanded && (
50 |
51 | {items && items.map((item) => (
52 |
53 |
57 |
58 |
{item.label}
59 |
60 |
61 | {format(new Date(item.lastEdit), "MM/dd HH:mm")}
62 |
63 |
64 | ))}
65 |
66 | )}
67 |
68 | )
69 | }
--------------------------------------------------------------------------------
/app/(routes)/dashboard/_components/webs-browser.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | interface WebsBrowserProps {
4 | url: string
5 | }
6 |
7 | export const WebsBrowser = ({
8 | url
9 | }: WebsBrowserProps) => {
10 | return (
11 |
12 |
13 | Embed External Site
14 |
15 |
23 |
24 | )
25 | }
--------------------------------------------------------------------------------
/app/(routes)/dashboard/layout.tsx:
--------------------------------------------------------------------------------
1 | import { Sidebar } from "@/components/sidebar"
2 | import { Navbar } from "@/components/navbar"
3 |
4 |
5 | const DashboardLayout = ({
6 | children
7 | }: {
8 | children: React.ReactNode
9 | }) => {
10 | return (
11 |
12 |
13 |
14 |
15 |
16 | {children}
17 |
18 |
19 |
20 |
21 | )
22 | }
23 |
24 | export default DashboardLayout
--------------------------------------------------------------------------------
/app/api/category/[categoryId]/route.ts:
--------------------------------------------------------------------------------
1 | import { archiveCategory, favoriteCategory, moveCategory, removeCategory, renameCategory } from "@/data/category"
2 | import { NextResponse } from "next/server"
3 |
4 | export const PATCH = async (
5 | req: Request,
6 | { params }: { params: { categoryId: string } }
7 | ) => {
8 | try {
9 | const body = await req.json()
10 | const { title, favorited, archived, parentId } = body
11 | if (title) {
12 | const renamedCategory = await renameCategory(params.categoryId, title);
13 | if (renamedCategory) {
14 | return new NextResponse(JSON.stringify(renamedCategory), { status: 200 });
15 | }
16 | return new NextResponse("Failed to update category", { status: 500 });
17 | } else if (parentId) {
18 | const movedCategory = await moveCategory(params.categoryId, parentId);
19 | if (movedCategory) {
20 | return new NextResponse(JSON.stringify(movedCategory), { status: 200 });
21 | }
22 | return new NextResponse("Failed to move category", { status: 500 });
23 | } else if (archived === true) {
24 | const archivedCategory = await archiveCategory(params.categoryId, true)
25 | if (archivedCategory) {
26 | return new NextResponse(JSON.stringify(archivedCategory), { status: 200 })
27 | }
28 | return new NextResponse("Failed to update category", { status: 500 })
29 | } else if (archived === false) {
30 | const restoredCategory = await archiveCategory(params.categoryId, false)
31 | if (restoredCategory) {
32 | return new NextResponse(JSON.stringify(restoredCategory), { status: 200 })
33 | }
34 | return new NextResponse("Failed to update category", { status: 500 })
35 | } else if (favorited === true) {
36 | const favoritedCategory = await favoriteCategory(params.categoryId, true)
37 | if (favoritedCategory) {
38 | return new NextResponse(JSON.stringify(favoritedCategory), { status: 200 })
39 | }
40 | return new NextResponse("Failed to update category", { status: 500 })
41 | } else if (favorited === false) {
42 | const unfavoritedCategory = await favoriteCategory(params.categoryId, false)
43 | if (unfavoritedCategory) {
44 | return new NextResponse(JSON.stringify(unfavoritedCategory), { status: 200 })
45 | }
46 | return new NextResponse("Failed to update category", { status: 500 })
47 | } else {
48 | return new NextResponse("Unexpected field", { status: 404 })
49 | }
50 | } catch (error) {
51 | console.log("UPDATE CATEGORY ERROR", error)
52 | return new NextResponse("Internal Error", { status: 500 })
53 | }
54 | }
55 |
56 | export const DELETE = async (
57 | request: Request,
58 | { params }: { params: { categoryId: string } }
59 | ) => {
60 | try {
61 | const removedCategory = await removeCategory(params.categoryId)
62 | if (removedCategory) {
63 | return new NextResponse("Category removed", { status: 200 })
64 | }
65 | } catch (error) {
66 | console.log("DELETE CATEGORY ERROR", error)
67 | return new NextResponse("Internal Error", { status: 500 })
68 | }
69 | }
--------------------------------------------------------------------------------
/app/api/category/route.ts:
--------------------------------------------------------------------------------
1 | import { createCategory, getAllCategories } from "@/data/category";
2 | import { NextResponse } from "next/server";
3 |
4 | export const POST = async (req: Request) => {
5 | try {
6 | const body = await req.json();
7 | const { name, type, parentId } = body
8 | if (!name || name === "") {
9 | return new NextResponse("name must not be null", { status: 400 });
10 | }
11 | if (!parentId || parentId === "") {
12 | return new NextResponse("parentId must not be null", { status: 400 });
13 | }
14 | if (!type || type === "") {
15 | return new NextResponse("category type must not be null", { status: 400 });
16 | }
17 | const category = await createCategory(name, type, parentId)
18 | return NextResponse.json(category)
19 | } catch (error) {
20 | console.log("CREATE CATEGORY ERROR", error);
21 | return new NextResponse("Internal Error", { status: 500 });
22 | }
23 | }
24 |
25 | export const GET = async () => {
26 | try {
27 | const categories = await getAllCategories()
28 | return NextResponse.json(categories)
29 | } catch (error) {
30 | console.log("GET CATEGORIES ERROR", error);
31 | return new NextResponse("Internal Error", { status: 500 });
32 | }
33 | }
--------------------------------------------------------------------------------
/app/api/chat/route.ts:
--------------------------------------------------------------------------------
1 | import { OpenAIChat } from "@/ai/openai";
2 | import { saveFileToVectorstore } from "@/ai/vectorstore";
3 | import { StreamingTextResponse } from "ai";
4 | import { NextRequest, NextResponse } from "next/server";
5 |
6 | export const POST = async (req: NextRequest) => {
7 | try {
8 | const body = await req.json()
9 | const { paperId, paperUrl, prompt, prevMessages } = body
10 | // await saveFileToVectorstore(paperId, paperUrl)
11 | const stream = await OpenAIChat(paperId, prompt, prevMessages)
12 | // const stream = await BaiduChat(paperId, prompt, prevMessages)
13 | if (stream) {
14 | return new StreamingTextResponse(stream);
15 | } else {
16 | return new NextResponse("Openai chat error", { status: 500 });
17 | }
18 | } catch (error) {
19 | console.log("CHAT WITH AI ERROR", error)
20 | return new NextResponse("Chat with ai error", { status: 500 })
21 | }
22 | }
--------------------------------------------------------------------------------
/app/api/image/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 | import { promises as fs } from 'fs';
3 |
4 | export async function POST(req: Request) {
5 | try {
6 | const formData = await req.formData();
7 | const image = formData.get("image") as File;
8 | if (!image) {
9 | return new NextResponse("No image uploaded", { status: 404 });
10 | }
11 | // 处理文件的保存
12 | const imageNameSplits = image.name.split(".")
13 | if (imageNameSplits.length === 0) {
14 | return new NextResponse("Uploaded file is not a valid image", { status: 404 });
15 | }
16 | const url = `/images/${Date.now().toString()}.${imageNameSplits[imageNameSplits.length - 1]}`;
17 | const imageData = await image.arrayBuffer();
18 | const buffer = Buffer.from(imageData);
19 | await fs.writeFile(`public${url}`, buffer);
20 | // remove old image
21 | const oldUrl = formData.get("oldUrl")
22 | if (oldUrl) {
23 | const resoveUrl = oldUrl as string
24 | await fs.unlink(resoveUrl)
25 | }
26 | return new NextResponse(url, { status: 200 });
27 | } catch (err) {
28 | console.log("UPLOAD IMAGE ERROR", err);
29 | return new NextResponse("Upload image error", { status: 500 });
30 | }
31 | }
32 |
33 |
--------------------------------------------------------------------------------
/app/api/message/route.ts:
--------------------------------------------------------------------------------
1 | import { createMessage, getMessages, getLimitedMessages, removeMessagesOfPaper } from "@/data/message";
2 | import { NextResponse } from "next/server";
3 |
4 | export const POST = async (req: Request) => {
5 | try {
6 | const body = await req.json();
7 | const { paperId, role, content } = body
8 | if (!paperId || paperId === "") {
9 | return new NextResponse("paperId must not be null", { status: 400 });
10 | }
11 | if (!role || role === "") {
12 | return new NextResponse("role must not be null", { status: 400 });
13 | }
14 | if (!content || content === "") {
15 | return new NextResponse("content must not be null", { status: 400 });
16 | }
17 | const message = await createMessage(paperId, role, content)
18 | return NextResponse.json(message)
19 | } catch (error) {
20 | console.log("CREATE MESSAGE ERROR", error);
21 | return new NextResponse("Internal Error", { status: 500 });
22 | }
23 | }
24 |
25 | export const GET = async (req: Request,
26 | { params }: {
27 | params: {
28 | paperId: string,
29 | limit: number
30 | }
31 | }
32 | ) => {
33 | try {
34 | if (!params.limit) {
35 | const messages = await getMessages(params.paperId)
36 | return NextResponse.json(messages)
37 | } else {
38 | const messages = await getLimitedMessages(params.paperId, params.limit)
39 | return NextResponse.json(messages)
40 | }
41 | } catch (error) {
42 | console.log("GET MESSAGES ERROR", error);
43 | return new NextResponse("Internal Error", { status: 500 });
44 | }
45 | }
46 |
47 | export const DELETE = async (
48 | request: Request,
49 | { params }: { params: { paperId: string } }
50 | ) => {
51 | try {
52 | const removedMessages = await removeMessagesOfPaper(params.paperId)
53 | if (removedMessages) {
54 | return new NextResponse("Note removed", { status: 200 })
55 | }
56 | } catch (error) {
57 | console.log("DELETE NOTE ERROR", error)
58 | return new NextResponse("Internal Error", { status: 500 })
59 | }
60 | }
--------------------------------------------------------------------------------
/app/api/note/[noteId]/route.ts:
--------------------------------------------------------------------------------
1 | import { archiveNote, favoriteNote, removeNote, renameNote, updateContent, updateIcon } from "@/data/note"
2 | import { NextResponse } from "next/server"
3 |
4 | export const PATCH = async (
5 | req: Request,
6 | { params }: { params: { noteId: string } }
7 | ) => {
8 | try {
9 | const body = await req.json()
10 | const { title, archived, favorited, content, icon } = body
11 | if (title) {
12 | const renamedNote = await renameNote(params.noteId, title)
13 | if (renamedNote) {
14 | return NextResponse.json(renamedNote)
15 | }
16 | return new NextResponse("Failed to update note", { status: 500 })
17 | } else if (icon) {
18 | const updatedNote = await updateIcon(params.noteId, icon)
19 | if (updatedNote) {
20 | return NextResponse.json(updatedNote)
21 | }
22 | return new NextResponse("Failed to update note", { status: 500 })
23 | } else if (favorited === true) {
24 | const favoritedNote = await favoriteNote(params.noteId, true)
25 | if (favoritedNote) {
26 | return NextResponse.json(favoritedNote)
27 | }
28 | return new NextResponse("Failed to update note", { status: 500 })
29 | } else if (favorited === false) {
30 | const unfavoritedNote = await favoriteNote(params.noteId, false)
31 | if (unfavoritedNote) {
32 | return NextResponse.json(unfavoritedNote)
33 | }
34 | return new NextResponse("Failed to update note", { status: 500 })
35 | } else if (archived === true) {
36 | const archivedNote = await archiveNote(params.noteId, true)
37 | if (archivedNote) {
38 | return NextResponse.json(archivedNote)
39 | }
40 | return new NextResponse("Failed to update note", { status: 500 })
41 | } else if (archived === false) {
42 | const restoredNote = await archiveNote(params.noteId, false)
43 | if (restoredNote) {
44 | return NextResponse.json(restoredNote)
45 | }
46 | return new NextResponse("Failed to update note", { status: 500 })
47 | } else if (content !== null) {
48 | const updatedNote = await updateContent(params.noteId, content)
49 | if (updatedNote) {
50 | return NextResponse.json(updatedNote)
51 | }
52 | return new NextResponse("Failed to update note", { status: 500 })
53 | } else {
54 | return new NextResponse("Unexpected field", { status: 404 })
55 | }
56 | } catch (error) {
57 | console.log("UPDATE NOTE ERROR", error)
58 | return new NextResponse("Internal Error", { status: 500 })
59 | }
60 | }
61 |
62 | export const DELETE = async (
63 | request: Request,
64 | { params }: { params: { noteId: string } }
65 | ) => {
66 | try {
67 | const removedNote = await removeNote(params.noteId)
68 | if (removedNote) {
69 | return new NextResponse("Note removed", { status: 200 })
70 | }
71 | } catch (error) {
72 | console.log("DELETE NOTE ERROR", error)
73 | return new NextResponse("Internal Error", { status: 500 })
74 | }
75 | }
--------------------------------------------------------------------------------
/app/api/note/route.ts:
--------------------------------------------------------------------------------
1 | import { createNote } from "@/data/note";
2 | import { NextResponse } from "next/server";
3 |
4 | export const POST = async (req: Request) => {
5 | try {
6 | const body = await req.json();
7 | const { title, type, categoryId, paperId } = body
8 | if (!title) {
9 | return new NextResponse("title cannot be null", { status: 400 });
10 | }
11 | if (!type) {
12 | return new NextResponse("type cannot be null", { status: 400 });
13 | }
14 | if (!categoryId) {
15 | return new NextResponse("categoryId cannot be null", { status: 400 });
16 | }
17 | const note = await createNote(title, type, categoryId, paperId)
18 | return NextResponse.json(note)
19 | } catch (error) {
20 | console.log("CREATE NOTE ERROR", error);
21 | return new NextResponse("Internal Error", { status: 500 });
22 | }
23 | }
--------------------------------------------------------------------------------
/app/api/paper/[paperId]/route.ts:
--------------------------------------------------------------------------------
1 | import { archivePaper, favoritePaper, getPaperById, movePaper, removePaper, renamePaper, updateAnnotionOfPaper } from "@/data/paper"
2 | import { NextResponse } from "next/server"
3 | import path from "path"
4 | import fs from 'fs';
5 | import { removeNotesOfPaper } from "@/data/note";
6 | import { removeMessagesOfPaper } from "@/data/message";
7 |
8 | export const PATCH = async (
9 | req: Request,
10 | { params }: { params: { paperId: string } }
11 | ) => {
12 | try {
13 | const body = await req.json()
14 | const { title, archived, favorited, annotations, parentId } = body
15 | if (title) {
16 | const renamedPaper = await renamePaper(params.paperId, title)
17 | if (renamedPaper) {
18 | return NextResponse.json(renamedPaper)
19 | }
20 | return new NextResponse("Failed to update paper", { status: 500 })
21 | } else if (parentId) {
22 | const movedPaper = await movePaper(params.paperId, parentId)
23 | if (movedPaper) {
24 | return NextResponse.json(movedPaper)
25 | }
26 | return new NextResponse("Failed to move paper", { status: 500 })
27 | } else if (archived === true) {
28 | const archivedPaper = await archivePaper(params.paperId, true)
29 | if (archivedPaper) {
30 | return NextResponse.json(archivedPaper)
31 | }
32 | return new NextResponse("Failed to update paper", { status: 500 })
33 | } else if (archived === false) {
34 | const restoredPaper = await archivePaper(params.paperId, false)
35 | if (restoredPaper) {
36 | return NextResponse.json(restoredPaper)
37 | }
38 | return new NextResponse("Failed to update paper", { status: 500 })
39 | } else if (favorited === true) {
40 | const favoritedPaper = await favoritePaper(params.paperId, true)
41 | if (favoritedPaper) {
42 | return NextResponse.json(favoritedPaper)
43 | }
44 | return new NextResponse("Failed to update paper", { status: 500 })
45 | } else if (favorited === false) {
46 | const unfavoritedPaper = await favoritePaper(params.paperId, false)
47 | if (unfavoritedPaper) {
48 | return NextResponse.json(unfavoritedPaper)
49 | }
50 | return new NextResponse("Failed to update paper", { status: 500 })
51 | } else if (annotations === "" || annotations) {
52 | const updateAnnotations = await updateAnnotionOfPaper(params.paperId, annotations)
53 | if (updateAnnotations) {
54 | return NextResponse.json(updateAnnotations)
55 | }
56 | return new NextResponse("Failed to update paper", { status: 500 })
57 | } else {
58 | return new NextResponse("Unexpected field", { status: 404 })
59 | }
60 | } catch (error) {
61 | console.log("UPDATE PAPER ERROR", error)
62 | return new NextResponse("Internal Error", { status: 500 })
63 | }
64 | }
65 |
66 | export const DELETE = async (
67 | request: Request,
68 | { params }: { params: { paperId: string } }
69 | ) => {
70 | try {
71 | const removedPaper = await removePaper(params.paperId)
72 | if (removedPaper) {
73 | // 删除对应的notes
74 | await removeNotesOfPaper(removedPaper.id)
75 | // 删除对应的messages
76 | await removeMessagesOfPaper(removedPaper.id)
77 | // 删除对应的向量数据库文件(TODO)
78 | // 删除对应的pdf文件
79 | const filePath = path.join(process.cwd(), "public", removedPaper.url)
80 | if (fs.existsSync(filePath)) {
81 | try {
82 | fs.unlinkSync(filePath)
83 | console.log("PDF FILE REMOVED", filePath)
84 | } catch (error) {
85 | console.log("REMOVE PAPER PDF FILE ERROR", error)
86 | }
87 | }
88 | return new NextResponse("Paper removed", { status: 200 })
89 | }
90 | } catch (error) {
91 | console.log("DELETE PAPER ERROR", error)
92 | return new NextResponse("Internal Error", { status: 500 })
93 | }
94 | }
95 |
96 | export const GET = async (
97 | req: Request,
98 | { params }: { params: { paperId: string } }
99 | ) => {
100 | try {
101 | const paper = await getPaperById(params.paperId)
102 | if (paper) {
103 | return NextResponse.json(paper)
104 | }
105 | return new NextResponse("Paper not found", { status: 404 })
106 | } catch (error) {
107 | console.log("GET PAPER ERROR", error)
108 | return new NextResponse("Internal Error", { status: 500 })
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/app/api/paper/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 | import { promises as fs } from 'fs';
3 | import { createPaper } from "@/data/paper";
4 | import { createNote } from "@/data/note";
5 | import PDFParser from "pdf2json";
6 |
7 | const parsePdfDate = (dateStr: string): Date | null => {
8 | // 正则表达式来提取日期部分
9 | const match = /D:(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})Z/.exec(dateStr);
10 | if (match) {
11 | const year = parseInt(match[1], 10);
12 | const month = parseInt(match[2], 10) - 1; // 月份从 0 开始计数
13 | const day = parseInt(match[3], 10);
14 | const hour = parseInt(match[4], 10);
15 | const minute = parseInt(match[5], 10);
16 | const second = parseInt(match[6], 10);
17 |
18 | // 创建一个 UTC 日期对象
19 | return new Date(Date.UTC(year, month, day, hour, minute, second));
20 | }
21 | return null; // 如果不匹配,返回 null
22 | }
23 |
24 | export const POST = async (req: NextRequest) => {
25 | try {
26 | const formData = await req.formData()
27 | const paper = formData.get("paper") as any;
28 | const title = formData.get("title") as string;
29 | const categoryId = formData.get("categoryId") as string;
30 |
31 | if (!paper) {
32 | return new NextResponse("No paper uploaded", { status: 404 });
33 | }
34 |
35 | if (!title) {
36 | return new NextResponse("Title is not specified", { status: 404 });
37 | }
38 |
39 | // 处理文件的保存
40 | const url = `/papers/${Date.now().toString()}.pdf`;
41 | const data = await paper.arrayBuffer();
42 | const buffer = Buffer.from(data);
43 |
44 | const pdfData = await new Promise((resolve, reject) => {
45 | const pdfParser = new PDFParser()
46 | pdfParser.on("pdfParser_dataError", (errData: any) => {
47 | reject(errData)
48 | })
49 | pdfParser.on("pdfParser_dataReady", async (pdfData: any) => {
50 | resolve(pdfData)
51 | })
52 | pdfParser.parseBuffer(buffer)
53 | })
54 | const paperTitle = (pdfData as any).Meta.Title ? (pdfData as any).Meta.Title : title
55 | const authors = (pdfData as any).Meta.Author
56 | const publication = (pdfData as any).Meta.Subject
57 | const publicateDate = (pdfData as any).Meta.publicateDate
58 | let date = undefined
59 | if (publicateDate) {
60 | date = parsePdfDate(publicateDate)?.toDateString()
61 | }
62 | await fs.writeFile(`public${url}`, buffer);
63 | const paperInfo = await createPaper(paperTitle, url, categoryId, authors, publication, publicateDate)
64 | if (paperInfo) {
65 | await createNote(`《${paperInfo.title}》的笔记`, "Markdown", "notes", paperInfo.id)
66 | await createNote(`《${paperInfo.title}》的白板`, "Whiteboard", "whiteboards", paperInfo.id)
67 | return NextResponse.json(paperInfo);
68 | }
69 | return new NextResponse("Internal server error", { status: 500 });
70 | } catch (error) {
71 | console.log("UPLOAD PAPER ERROR", error);
72 | return new NextResponse("Internal server error", { status: 500 });
73 | }
74 | }
--------------------------------------------------------------------------------
/app/api/translate/route.ts:
--------------------------------------------------------------------------------
1 |
2 | import { NextRequest, NextResponse } from "next/server";
3 | import crypto from 'crypto';
4 | import axios from "axios";
5 |
6 | export const POST = async (req: NextRequest) => {
7 | const body = await req.json();
8 | const { text } = body;
9 | const salt = new Date().getTime();
10 | const appid = process.env.BAIDU_TRANSLATION_APPID
11 | const key = process.env.BAIDU_TRANSLATION_SECRET_KEY
12 | const domain = 'academic';
13 | const from = 'en';
14 | const to = 'zh';
15 | const str1 = appid + text + salt + domain + key;
16 | const sign = crypto.createHash('md5').update(str1).digest('hex');
17 | const response = await axios.get(
18 | "https://fanyi-api.baidu.com/api/trans/vip/fieldtranslate",
19 | {
20 | params: {
21 | q: text,
22 | appid: appid,
23 | salt: salt,
24 | domain: domain,
25 | from: from,
26 | to: to,
27 | sign: sign
28 | },
29 | }
30 | )
31 | if (response.status !== 200) {
32 | return new NextResponse("translate error", { status: 500 });
33 | }
34 | if (response.data.trans_result.length > 0) {
35 | return new NextResponse(response.data.trans_result[0].dst, { status: 200 })
36 | } else {
37 | return new NextResponse("translate error", { status: 500 });
38 | }
39 |
40 | }
--------------------------------------------------------------------------------
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tsmotlp/knowtate/fe55070afb0379468895d5ca2866bf5cb99a8bc6/app/favicon.ico
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | html,
6 | body,
7 | :root {
8 | height: 100%;
9 | }
10 |
11 | @layer base {
12 | :root {
13 | --background: 0 0% 100%;
14 | --foreground: 222.2 84% 4.9%;
15 | --card: 0 0% 100%;
16 | --card-foreground: 222.2 84% 4.9%;
17 | --popover: 0 0% 100%;
18 | --popover-foreground: 222.2 84% 4.9%;
19 | --primary: 221.2 83.2% 53.3%;
20 | --primary-foreground: 210 40% 98%;
21 | --secondary: 210 40% 96.1%;
22 | --secondary-foreground: 222.2 47.4% 11.2%;
23 | --muted: 210 40% 96.1%;
24 | --muted-foreground: 215.4 16.3% 46.9%;
25 | --accent: 210 40% 96.1%;
26 | --accent-foreground: 222.2 47.4% 11.2%;
27 | --destructive: 0 84.2% 60.2%;
28 | --destructive-foreground: 210 40% 98%;
29 | --border: 214.3 31.8% 91.4%;
30 | --input: 214.3 31.8% 91.4%;
31 | --ring: 221.2 83.2% 53.3%;
32 | --radius: 0.5rem;
33 | }
34 |
35 | .dark {
36 | --background: 222.2 84% 4.9%;
37 | --foreground: 210 40% 98%;
38 | --card: 222.2 84% 4.9%;
39 | --card-foreground: 210 40% 98%;
40 | --popover: 222.2 84% 4.9%;
41 | --popover-foreground: 210 40% 98%;
42 | --primary: 217.2 91.2% 59.8%;
43 | --primary-foreground: 222.2 47.4% 11.2%;
44 | --secondary: 217.2 32.6% 17.5%;
45 | --secondary-foreground: 210 40% 98%;
46 | --muted: 217.2 32.6% 17.5%;
47 | --muted-foreground: 215 20.2% 65.1%;
48 | --accent: 217.2 32.6% 17.5%;
49 | --accent-foreground: 210 40% 98%;
50 | --destructive: 0 62.8% 30.6%;
51 | --destructive-foreground: 210 40% 98%;
52 | --border: 217.2 32.6% 17.5%;
53 | --input: 217.2 32.6% 17.5%;
54 | --ring: 224.3 76.3% 48%;
55 | }
56 | }
57 |
58 | @layer base {
59 | * {
60 | @apply border-border;
61 | }
62 |
63 | body {
64 | @apply bg-background text-foreground;
65 | }
66 | }
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { Outfit } from "next/font/google";
3 | import "./globals.css";
4 | import { Toaster } from "sonner";
5 |
6 | const outfit = Outfit({ subsets: ["latin"] });
7 |
8 | export const metadata: Metadata = {
9 | title: "Knowtate",
10 | description: "Generated by create next app",
11 | };
12 |
13 | export default function RootLayout({
14 | children,
15 | }: Readonly<{
16 | children: React.ReactNode;
17 | }>) {
18 | return (
19 |
20 |
21 |
22 | {children}
23 |
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/app/notes/[noteId]/page.tsx:
--------------------------------------------------------------------------------
1 | import { PaperNote } from "@/app/papers/[paperId]/_components/paper-note"
2 | import { getNoteWithPaper } from "@/data/note";
3 |
4 | interface NoteIdPageProps {
5 | params: {
6 | noteId: string;
7 | };
8 | }
9 |
10 | const NoteIdPage = async ({ params }: NoteIdPageProps) => {
11 | const note = await getNoteWithPaper(params.noteId)
12 | return (
13 |
14 | {note ? (
15 |
18 | ) : (
19 |
20 | 出错了
21 |
22 | )}
23 |
24 | )
25 | }
26 |
27 | export default NoteIdPage
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | import { Heading } from "@/components/heading";
3 | import { Heroes } from "@/components/heros";
4 | import { Button } from "@/components/ui/button";
5 | import { ArrowRight } from "lucide-react";
6 | import Image from "next/image";
7 | import Link from "next/link";
8 | import { useRouter } from "next/navigation";
9 |
10 | export default function Home() {
11 | const router = useRouter()
12 | return (
13 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/app/papers/[paperId]/_components/chat/chat-client.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { FormEvent, useState } from "react";
3 | import { ChatForm } from "./chat-form";
4 | import { ChatMessages } from "./chat-messages";
5 | import { useRouter } from "next/navigation";
6 | import { ChatMessageProps } from "./chat-message";
7 | import Image from "next/image";
8 | import { HISTORY_MESSAGE_N } from "@/constants";
9 | import { Message } from "@prisma/client";
10 | import axios from "axios";
11 | import { toast } from "sonner";
12 |
13 | interface ChatClientProps {
14 | paperId: string
15 | paperUrl: string,
16 | messages: Message[]
17 | }
18 |
19 | export const ChatClient = ({
20 | paperId,
21 | paperUrl,
22 | messages
23 | }: ChatClientProps) => {
24 | const router = useRouter();
25 | const [historyMessages, setHistoryMessages] = useState(messages)
26 | const [input, setInput] = useState("");
27 | const [isLoading, setIsLoading] = useState(false); // 使用 isLoading 状态
28 | const handleStream = async () => {
29 | let fullMessage = ''; // 用来累积完整的消息
30 | try {
31 | setIsLoading(true)
32 | const data = {
33 | paperId: paperId,
34 | paperUrl: paperUrl,
35 | prompt: input,
36 | prevMessages: historyMessages.slice(-1 * HISTORY_MESSAGE_N),
37 | }
38 | const response = await fetch("/api/chat", {
39 | method: "POST",
40 | headers: { "Content-Type": "application/json" },
41 | body: JSON.stringify(data)
42 | });
43 |
44 | const reader = response.body!.getReader();
45 | const decoder = new TextDecoder("utf-8");
46 |
47 | const userMessage: ChatMessageProps = {
48 | role: "user",
49 | content: input
50 | }
51 | setHistoryMessages((current) => [...current, userMessage])
52 | while (true) {
53 | const { done, value } = await reader.read();
54 | if (done) break;
55 | const chunk = decoder.decode(value, { stream: true }); // 确保文本流不会被截断
56 | // 解析并处理数据
57 | const lines = chunk.split('\n');
58 | for (const line of lines) {
59 | if (line.trim() === '') continue;
60 | // 根据实际数据格式选择解析方式
61 | const match = line.match(/^0:"(.*)"$/);
62 |
63 | if (match) {
64 | const content = match[1];
65 | fullMessage += content;
66 |
67 | setHistoryMessages((current) => {
68 | const lastMessage = current[current.length - 1];
69 | if (lastMessage && lastMessage.role === 'assistant') {
70 | return current.slice(0, -1).concat({ ...lastMessage, content: fullMessage });
71 | }
72 | return current.concat({ role: 'assistant', content: fullMessage });
73 | });
74 | } else {
75 | console.warn('Unrecognized line format:', line);
76 | }
77 | }
78 | }
79 | await axios.post("/api/message", {
80 | paperId: paperId,
81 | role: "assistant",
82 | content: fullMessage,
83 | })
84 | } catch (error: any) {
85 | toast.error("Something went wrong.");
86 | } finally {
87 | setIsLoading(false);
88 | router.refresh()
89 | }
90 | };
91 |
92 | const handleSubmit = async (e: FormEvent) => {
93 | e.preventDefault();
94 | await axios.post("/api/message", JSON.stringify({
95 | paperId: paperId,
96 | role: "user",
97 | content: input,
98 | }))
99 | await handleStream(); // 等待处理新消息
100 | setInput(''); // 清空输入框
101 | };
102 |
103 | const handleInputChange = (e: React.ChangeEvent | React.ChangeEvent) => {
104 | // 函数实现
105 | setInput(e.target.value);
106 | };
107 |
108 | return (
109 |
110 |
111 |
112 |
113 |
114 |
120 |
121 |
122 | );
123 | };
124 |
--------------------------------------------------------------------------------
/app/papers/[paperId]/_components/chat/chat-form.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Button } from "@/components/ui/button";
4 | import { Input } from "@/components/ui/input";
5 | import { ChatRequestOptions } from "ai";
6 | import { SendHorizonal } from "lucide-react";
7 | import { ChangeEvent, FormEvent } from "react";
8 |
9 | interface ChatFormProps {
10 | input: string;
11 | handleInputChange: (
12 | e: ChangeEvent | ChangeEvent,
13 | ) => void;
14 | onSubmit: (
15 | e: FormEvent,
16 | chatRequestOptions?: ChatRequestOptions | undefined,
17 | ) => void;
18 | isLoading: boolean;
19 | }
20 |
21 | export const ChatForm = ({
22 | input,
23 | handleInputChange,
24 | onSubmit,
25 | isLoading,
26 | }: ChatFormProps) => {
27 | return (
28 |
43 | );
44 | };
45 |
--------------------------------------------------------------------------------
/app/papers/[paperId]/_components/chat/chat-message.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { BeatLoader } from "react-spinners";
4 | import { Copy } from "lucide-react";
5 |
6 | import { cn } from "@/lib/utils";
7 | import { Button } from "@/components/ui/button";
8 | import { toast } from "sonner";
9 | import ReactMarkdown from "react-markdown";
10 | import React from "react";
11 | import { Avatar, AvatarImage } from "@/components/ui/avatar";
12 |
13 | export interface ChatMessageProps {
14 | role: string;
15 | content?: string;
16 | isLoading?: boolean;
17 | }
18 |
19 | export const ChatMessage = ({ role, content, isLoading }: ChatMessageProps) => {
20 | const onCopy = () => {
21 | if (!content) {
22 | return;
23 | }
24 |
25 | navigator.clipboard.writeText(content);
26 | toast("", {
27 | description: "Message copied to clipboard.",
28 | duration: 3000,
29 | });
30 | };
31 |
32 | return (
33 |
39 | {role !== "user" && (
40 |
41 |
42 |
43 | )}
44 |
45 | {isLoading ? (
46 |
47 | ) : (
48 |
(
51 |
54 | ),
55 | code: ({ node, ...props }) => (
56 |
57 | ),
58 | }}
59 | className="text-sm overflow-hidden leading-7"
60 | >
61 | {content || ""}
62 |
63 | )}
64 |
65 | {role === "user" && (
66 |
67 |
68 |
69 | )}
70 | {role !== "user" && !isLoading && (
71 |
77 |
78 |
79 | )}
80 |
81 | );
82 | };
83 |
--------------------------------------------------------------------------------
/app/papers/[paperId]/_components/chat/chat-messages.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { ElementRef, useEffect, useRef } from "react";
4 |
5 | import { ChatMessage, ChatMessageProps } from "./chat-message";
6 |
7 | interface ChatMessagesProps {
8 | messages: ChatMessageProps[];
9 | isLoading: boolean;
10 | }
11 |
12 | export const ChatMessages = ({
13 | messages = [],
14 | isLoading,
15 | }: ChatMessagesProps) => {
16 | const scrollRef = useRef>(null);
17 | useEffect(() => {
18 | scrollRef?.current?.scrollIntoView({ behavior: "smooth" });
19 | }, [messages.length]);
20 |
21 | return (
22 |
23 | {messages.map((message, index) => (
24 |
29 | ))}
30 | {isLoading && (
31 |
32 | )}
33 |
34 |
35 | );
36 | };
37 |
--------------------------------------------------------------------------------
/app/papers/[paperId]/_components/menu-bar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { Button } from "@/components/ui/button"
4 | import { BotMessageSquareIcon, InfoIcon, Library, NotebookTabsIcon } from "lucide-react"
5 | import { useRouter } from "next/navigation"
6 | import { ToolButton } from "./tool-button"
7 |
8 | interface MenuBarProps {
9 | paperTitle: string
10 | pageType: string,
11 | handleClick: (type: string) => void
12 | }
13 |
14 | export const MenuBar = ({
15 | paperTitle,
16 | pageType,
17 | handleClick
18 | }: MenuBarProps) => {
19 | const router = useRouter()
20 | return (
21 |
22 |
{ router.push("/dashboard/library") }}
28 | color=""
29 | />
30 |
31 | {paperTitle}
32 |
33 |
34 | {
38 | handleClick("info")
39 | }}
40 | >
41 |
42 |
43 | {
47 | handleClick("notes")
48 | }}
49 | >
50 |
51 |
52 | {
56 | handleClick("chat")
57 | }}
58 | >
59 |
60 |
61 |
62 |
63 | )
64 | }
--------------------------------------------------------------------------------
/app/papers/[paperId]/_components/note-navbar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { IconPicker } from "@/components/editors/icon-picker";
4 | import { Button } from "@/components/ui/button"
5 | import { Note, Paper } from "@prisma/client";
6 | import axios from "axios";
7 | import { Download, FileSymlinkIcon, Library, Save, Smile } from "lucide-react"
8 | import Link from "next/link";
9 | import { useRouter } from "next/navigation";
10 | import { ElementRef, useRef, useState } from "react";
11 | import TextareaAutosize from "react-textarea-autosize";
12 | import { ToolButton } from "./tool-button";
13 | import { BsFiletypePdf } from "react-icons/bs";
14 |
15 | interface NoteNavbarProps {
16 | paper?: Paper,
17 | note: Note,
18 | handleSave: () => void
19 | handleDownload: (title: string) => void
20 | showDashboardIcon: boolean
21 | }
22 |
23 | export const NoteNavbar = ({
24 | paper,
25 | note,
26 | handleSave,
27 | handleDownload,
28 | showDashboardIcon,
29 | }: NoteNavbarProps) => {
30 | const router = useRouter();
31 | const inputRef = useRef>(null);
32 | const [isEditing, setIsEditing] = useState(false);
33 | const [title, setTitle] = useState(note.title);
34 | const [icon, setIcon] = useState(note.icon);
35 |
36 | const enableInput = () => {
37 | setIsEditing(true);
38 | setTimeout(() => {
39 | setTitle(note.title);
40 | inputRef.current?.focus();
41 | }, 0);
42 | };
43 |
44 | const disableInput = () => setIsEditing(false);
45 |
46 | const onInput = async (value: string) => {
47 | setTitle(value);
48 | try {
49 | await axios.patch(`/api/note/${note.id}`, JSON.stringify({
50 | title: value || "Untitled",
51 | }));
52 | } catch (error) {
53 | console.error("Failed to update note title:", error);
54 | }
55 | };
56 |
57 | const onKeyDown = (event: React.KeyboardEvent) => {
58 | if (event.key === "Enter") {
59 | event.preventDefault();
60 | disableInput();
61 | }
62 | };
63 |
64 | const onIconSelect = async (icon: string) => {
65 | try {
66 | await axios.patch(`/api/note/${note.id}`, JSON.stringify({
67 | icon: icon,
68 | }));
69 | setIcon(icon);
70 | } catch (error) {
71 | console.error("Failed to update note icon:", error);
72 | }
73 | };
74 |
75 | return (
76 |
77 |
78 | {showDashboardIcon && (
79 |
router.push("/dashboard/library")}
85 | color=""
86 | />
87 | )}
88 | {!!icon && (
89 |
90 |
91 | {icon}
92 |
93 |
94 | )}
95 | {!icon && (
96 |
97 |
102 |
103 |
104 |
105 | )}
106 | {isEditing ? (
107 | onInput(e.target.value)}
113 | className="truncate bg-transparent font-bold break-words outline-none text-[#3F3F3F] dark:text-[#CFCFCF] resize-none overflow-hidden"
114 | />
115 | ) : (
116 |
120 | {title}
121 |
122 | )}
123 |
124 |
125 |
133 |
{ handleDownload(title!); }}
139 | color=""
140 | />
141 | {paper && paper.id && (
142 |
143 |
144 |
145 | {paper.title}
146 |
147 |
148 | )}
149 |
150 |
151 | );
152 | };
153 |
--------------------------------------------------------------------------------
/app/papers/[paperId]/_components/note-sidebar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { Button } from "@/components/ui/button"
4 |
5 | interface NoteSidebarPorps {
6 | noteSidebarType: string
7 | handleClick: (key: string) => void
8 | }
9 |
10 | export const NoteSidebar = ({
11 | noteSidebarType = "markdown",
12 | handleClick
13 | }: NoteSidebarPorps) => {
14 | return (
15 |
16 | { handleClick("markdown") }}
20 | />
21 | { handleClick("whiteboard") }}
25 | />
26 | { handleClick("vocabs") }}
30 | />
31 | { handleClick("terms") }}
35 | />
36 | { handleClick("sentences") }}
40 | />
41 |
42 | )
43 | }
44 |
45 | interface NoteSidebarItemProps {
46 | title: string,
47 | active: boolean,
48 | handleClick: () => void
49 | }
50 |
51 | const NoteSidebarItem = ({
52 | title,
53 | active,
54 | handleClick
55 | }: NoteSidebarItemProps) => {
56 | return (
57 |
62 | {title}
63 |
64 | )
65 | }
--------------------------------------------------------------------------------
/app/papers/[paperId]/_components/paper-chat.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { Message, Paper } from "@prisma/client"
4 | import { ChatClient } from "./chat/chat-client"
5 |
6 | interface PaperChatProps {
7 | paperId: string
8 | messages: Message[]
9 | }
10 |
11 |
12 | export const PaperChat = ({
13 | paperId,
14 | messages
15 | }: PaperChatProps) => {
16 | return (
17 |
18 |
19 |
20 | )
21 | }
--------------------------------------------------------------------------------
/app/papers/[paperId]/_components/paper-info.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | export const PaperInfo = () => {
4 | return (
5 |
6 | Paper info
7 |
8 | )
9 | }
--------------------------------------------------------------------------------
/app/papers/[paperId]/_components/paper-note.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useEffect, useMemo, useState } from "react"
4 | import { toast } from "sonner"
5 | import '@mdxeditor/editor/style.css';
6 | import dynamic from "next/dynamic"
7 | import axios from "axios"
8 | import { NoteNavbar } from "./note-navbar"
9 | import { Note, Paper } from "@prisma/client";
10 | import { DashboardItemType } from "@/types/types";
11 |
12 | interface PaperNoteProps {
13 | paper?: Paper,
14 | note: Note,
15 | showDashboardIcon?: boolean
16 | }
17 |
18 | export const PaperNote = ({
19 | paper,
20 | note,
21 | showDashboardIcon = false
22 | }: PaperNoteProps) => {
23 | const MarkEditor = useMemo(() => dynamic(() => import("@/components/editors/markdown-editor"), { ssr: false }), []);
24 | const ExcalidrawEditor = useMemo(() => dynamic(() => import("@/components/editors/excalidraw-editor"), { ssr: false }), []);
25 |
26 | // 状态来跟踪编辑器的内容和最后一次更新的内容
27 | const [editorContent, setEditorContent] = useState(note.content ? note.content : "");
28 | const [lastSavedContent, setLastSavedContent] = useState(note.content ? note.content : "");
29 |
30 | // 修改 onChange 方法以保存当前编辑内容而不是立即更新 note
31 | const onChange = (content: string) => {
32 | setEditorContent(content);
33 | };
34 |
35 | // 用于自动保存的逻辑
36 | const autoSaveContent = async () => {
37 | if (editorContent !== lastSavedContent) {
38 | const response = await axios.patch(`/api/note/${note.id}`, JSON.stringify({
39 | content: editorContent
40 | }))
41 | if (!response || response.status !== 200) {
42 | toast.error("自动保存失败!")
43 | } else {
44 | setLastSavedContent(editorContent);
45 | toast.success("已自动保存!");
46 | }
47 | }
48 | };
49 |
50 | const handleSave = async () => {
51 | if (editorContent !== lastSavedContent) {
52 | const response = await axios.patch(`/api/note/${note.id}`, JSON.stringify({
53 | content: editorContent
54 | }))
55 | if (!response || response.status !== 200) {
56 | toast.error("保存失败!")
57 | } else {
58 | setLastSavedContent(editorContent);
59 | toast.success("已保存!");
60 | }
61 | }
62 | }
63 |
64 | const handleDownload = (title: string) => {
65 | const blob = new Blob([editorContent], { type: 'text/note;charset=utf-8' });
66 | const link = document.createElement('a');
67 | link.href = URL.createObjectURL(blob);
68 | link.download = `${title}.md`;
69 | link.click();
70 | }
71 |
72 | useEffect(() => {
73 | // 设置定时器每 10 秒自动保存
74 | const intervalId = setInterval(autoSaveContent, 10000);
75 |
76 | // 处理组件卸载和页面切换时的保存
77 | const handleBeforeUnload = async (e: BeforeUnloadEvent) => {
78 | await autoSaveContent();
79 | };
80 | window.addEventListener('beforeunload', handleBeforeUnload);
81 |
82 | // 清理定时器和事件监听器
83 | return () => {
84 | clearInterval(intervalId);
85 | window.removeEventListener('beforeunload', handleBeforeUnload);
86 | };
87 | }, [editorContent, note, lastSavedContent]);
88 |
89 | return (
90 |
91 |
92 |
99 |
100 | {note.type === DashboardItemType.Markdown && (
101 |
102 |
107 |
108 | )}
109 | {note.type === DashboardItemType.Whiteboard && (
110 |
111 |
117 |
118 | )}
119 |
120 | )
121 | }
--------------------------------------------------------------------------------
/app/papers/[paperId]/_components/paper-notes.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useState } from "react"
4 | import { PaperNote } from "./paper-note"
5 | import { NoteSidebar } from "./note-sidebar"
6 | import { Note } from "@prisma/client"
7 |
8 | interface PaperNotesProps {
9 | notes: Note[]
10 | }
11 |
12 | export const PaperNotes = ({
13 | notes
14 | }: PaperNotesProps) => {
15 | const [noteType, setNoteType] = useState("markdown")
16 | const handleClick = (key: string) => {
17 | setNoteType(key)
18 | }
19 | return (
20 |
21 |
22 | {noteType === "markdown" && (
23 |
24 | )}
25 | {noteType === "whiteboard" && (
26 |
27 | )}
28 |
29 |
30 |
31 |
32 |
33 | )
34 | }
--------------------------------------------------------------------------------
/app/papers/[paperId]/_components/paper-reader.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { MenuBar } from "./menu-bar"
4 | import { PDFViewer } from "./pdf-viewer"
5 | import { useState } from "react"
6 | import { PaperNotes } from "./paper-notes"
7 | import { PaperInfo } from "./paper-info"
8 | import { Message, Note, Paper } from "@prisma/client"
9 | import { ChatClient } from "./chat/chat-client"
10 | import { PaperNote } from "./paper-note"
11 |
12 | interface PaperReaderProps {
13 | paper: Paper & {
14 | notes: Note[]
15 | messages: Message[]
16 | }
17 | }
18 |
19 | export const PaperReader = ({
20 | paper
21 | }: PaperReaderProps) => {
22 | const [papeType, setPageType] = useState("notes")
23 | const handleClick = (type: string) => {
24 | setPageType(type)
25 | }
26 |
27 | return (
28 |
29 | {!paper || !paper.url || paper.url === "" ? (
30 | <>
31 |
32 |
33 |
34 |
35 | 出错了~
36 |
37 | >
38 | ) : (
39 | <>
40 |
41 |
42 |
43 |
44 |
45 |
48 |
49 | {papeType === "info" && (
50 |
51 | )}
52 | {papeType === "notes" && (
53 | //
54 |
55 | )}
56 | {papeType === "chat" && (
57 |
58 | )}
59 |
60 |
61 |
62 | >
63 | )}
64 |
65 | )
66 | }
--------------------------------------------------------------------------------
/app/papers/[paperId]/_components/style-setter.tsx:
--------------------------------------------------------------------------------
1 | // components/ColorPicker.tsx
2 | import { Button } from '@/components/ui/button';
3 | import { Slider } from '@/components/ui/slider';
4 | import { Circle, CircleCheck } from 'lucide-react';
5 | import React, { useState } from 'react';
6 |
7 | interface StyleSetterProps {
8 | initColor: string,
9 | initOpacity: number,
10 | handleColorChange: (color: string) => void
11 | handleOpacityChange: (opacity: number) => void
12 | }
13 |
14 | // 预定义的颜色列表
15 | const colors_row1 = [
16 | '#ff7002', '#ffc09e', '#ffd100', '#fcf485', '#0000ff', '#38e5ff'
17 | ];
18 |
19 | const colors_row2 = [
20 | '#a33086', '#fb88ff', '#e52237', '#ff809d', '#6ad928', '#c5fb72'
21 | ];
22 |
23 | const colors_row3 = [
24 | '#000000', '#444444', '#777777', '#aaaaaa', '#cccccc', '#ffffff'
25 | ];
26 |
27 | const StyleSetter = ({
28 | initColor,
29 | initOpacity,
30 | handleColorChange,
31 | handleOpacityChange
32 | }: StyleSetterProps) => {
33 | // 状态来追踪当前选中的颜色
34 | const [selectedColor, setSelectedColor] = useState(initColor);
35 | const [opacity, setOpacity] = useState(initOpacity)
36 |
37 |
38 | const handleOpacityValueChange = (value: number[]) => {
39 | setOpacity(value[0]); // 立即更新输入框的值
40 | handleOpacityChange(value[0])
41 | };
42 |
43 | return (
44 |
45 |
46 |
47 | {colors_row1.map((color, index) => (
48 |
49 | {
54 | setSelectedColor(color)
55 | handleColorChange(color)
56 | }}
57 | >
58 | {color === selectedColor ? (
59 |
60 | ) : (
61 |
62 | )}
63 |
64 |
65 | ))}
66 |
67 |
68 | {colors_row2.map((color, index) => (
69 |
70 | {
75 | setSelectedColor(color)
76 | handleColorChange(color)
77 | }}
78 | >
79 | {color === selectedColor ? (
80 |
81 | ) : (
82 |
83 | )}
84 |
85 |
86 | ))}
87 |
88 |
89 | {colors_row3.map((color, index) => (
90 |
91 | {
96 | setSelectedColor(color)
97 | handleColorChange(color)
98 | }}
99 | >
100 | {color === selectedColor ? (
101 |
102 | ) : (
103 |
104 | )}
105 |
106 |
107 | ))}
108 |
109 |
110 |
111 |
Opacity
112 |
113 |
120 | {opacity}
121 |
122 |
123 |
124 | );
125 | };
126 |
127 | export default StyleSetter;
128 |
--------------------------------------------------------------------------------
/app/papers/[paperId]/_components/tool-button.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { Button } from "@/components/ui/button"
4 | import {
5 | Tooltip,
6 | TooltipContent,
7 | TooltipProvider,
8 | TooltipTrigger
9 | } from "@/components/ui/tooltip"
10 | import { cn } from "@/lib/utils"
11 | import { LucideIcon } from "lucide-react"
12 |
13 | interface ToolButtonProps {
14 | label: string
15 | icon: LucideIcon
16 | size?: "icon" | "default" | "sm" | "lg" | "compact" | null | undefined,
17 | onClick: () => void
18 | iconClassName: string
19 | color: string
20 | }
21 |
22 | export const ToolButton = ({
23 | label,
24 | icon: Icon,
25 | size = "icon",
26 | onClick,
27 | iconClassName,
28 | color
29 | }: ToolButtonProps) => {
30 | return (
31 |
32 |
33 |
34 |
39 |
40 |
41 |
42 |
43 | {label}
44 |
45 |
46 |
47 | )
48 | }
--------------------------------------------------------------------------------
/app/papers/[paperId]/page.tsx:
--------------------------------------------------------------------------------
1 | import { getPaperWithNotesAndMessages } from "@/data/paper";
2 | import { PaperReader } from "./_components/paper-reader";
3 |
4 | interface PaperIdPageProps {
5 | params: {
6 | paperId: string;
7 | };
8 | }
9 |
10 | const PaperIdPage = async ({ params }: PaperIdPageProps) => {
11 | const paper = await getPaperWithNotesAndMessages(params.paperId)
12 | if (!paper) {
13 | return (
14 |
15 | 出错了~
16 |
17 | )
18 | }
19 | return (
20 |
21 | )
22 | }
23 |
24 | export default PaperIdPage
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "app/globals.css",
9 | "baseColor": "neutral",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils"
16 | }
17 | }
--------------------------------------------------------------------------------
/components/category-list.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { SidebarItem } from "@/components/sidebar-item"
4 | import { CategoryWithIcon } from "@/types/types"
5 | import { Category } from "@prisma/client"
6 | import axios from "axios"
7 | import { Folder } from "lucide-react"
8 | import { useParams, usePathname, useRouter } from "next/navigation"
9 | import { useEffect, useState } from "react"
10 | import { toast } from "sonner"
11 | import { defaultCategories } from "./default-categories"
12 |
13 |
14 | interface CategoryListProps {
15 | parentId: string
16 | categories: CategoryWithIcon[]
17 | level: number
18 | }
19 |
20 | export const CategoryList = ({
21 | parentId,
22 | categories,
23 | level,
24 | }: CategoryListProps) => {
25 | const params = useParams()
26 | const router = useRouter()
27 | const [expanded, setExpanded] = useState>({})
28 | const subCategories = categories.filter((category) => category.parentId === parentId)
29 | const onExpand = (categoryId: string) => {
30 | setExpanded(prevExpanded => ({
31 | ...prevExpanded,
32 | [categoryId]: !prevExpanded[categoryId]
33 | }));
34 | };
35 |
36 |
37 | if (!subCategories || subCategories.length === 0) {
38 | return (
39 | <>
40 |
41 | {level === 0 && (
42 | <>
43 |
44 |
45 | >
46 | )}
47 | >
48 | );
49 | }
50 | return (
51 | <>
52 | {subCategories && subCategories.length > 0 && subCategories
53 | .map((category) => (
54 |
55 | { router.push(`/dashboard/${category.id}`) }}
62 | onExpand={() => onExpand(category.id)}
63 | expanded={expanded[category.id]}
64 | showExpandIcon={categories.filter((item) => item.parentId === category.id).length > 0}
65 | />
66 | {expanded[category.id] && (
67 |
72 | )}
73 |
74 | ))
75 | }
76 | >
77 | )
78 |
79 | }
80 |
--------------------------------------------------------------------------------
/components/default-categories.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { CategoryType, CategoryWithIcon } from "@/types/types"
4 | import { BookA, BotIcon, FileType, FolderOpen, Globe, GraduationCap, HeartIcon, History, Library, NotebookPenIcon, PenTool, ReceiptText, Rss, Trash2 } from "lucide-react"
5 |
6 |
7 | export const defaultCategories: CategoryWithIcon[] = [
8 | {
9 | id: "library",
10 | name: "我的文库",
11 | type: CategoryType.Papers,
12 | favorited: false,
13 | archived: false,
14 | parentId: "text",
15 | icon: Library,
16 | createdAt: new Date(Date.now()),
17 | updatedAt: new Date(Date.now()),
18 | },
19 | // {
20 | // id: "webs",
21 | // name: "学术网站",
22 | // type: CategoryType.Others,
23 | // favorited: false,
24 | // archived: false,
25 | // parentId: "text",
26 | // icon: Globe,
27 | // createdAt: new Date(Date.now()),
28 | // updatedAt: new Date(Date.now()),
29 | // },
30 | {
31 | id: "notes",
32 | name: "我的笔记",
33 | type: CategoryType.Markdowns,
34 | favorited: false,
35 | archived: false,
36 | parentId: "",
37 | icon: NotebookPenIcon,
38 | createdAt: new Date(Date.now()),
39 | updatedAt: new Date(Date.now()),
40 | },
41 | {
42 | id: "favorites",
43 | name: "收藏夹",
44 | type: CategoryType.Others,
45 | favorited: false,
46 | archived: false,
47 | parentId: "text",
48 | icon: HeartIcon,
49 | createdAt: new Date(Date.now()),
50 | updatedAt: new Date(Date.now()),
51 | },
52 | {
53 | id: "trash",
54 | name: "回收站",
55 | type: CategoryType.Others,
56 | favorited: false,
57 | archived: false,
58 | parentId: "text",
59 | icon: Trash2,
60 | createdAt: new Date(Date.now()),
61 | updatedAt: new Date(Date.now()),
62 | },
63 | {
64 | id: "ai",
65 | name: "AI助手",
66 | type: CategoryType.Others,
67 | favorited: false,
68 | archived: false,
69 | parentId: "tool",
70 | icon: BotIcon,
71 | createdAt: new Date(Date.now()),
72 | updatedAt: new Date(Date.now()),
73 | },
74 | {
75 | id: "recents",
76 | name: "最近阅读",
77 | type: CategoryType.Papers,
78 | favorited: false,
79 | archived: false,
80 | parentId: "library",
81 | icon: History,
82 | createdAt: new Date(Date.now()),
83 | updatedAt: new Date(Date.now()),
84 | },
85 |
86 | {
87 | id: "whiteboards",
88 | name: "白板",
89 | type: CategoryType.Whiteboards,
90 | favorited: false,
91 | archived: false,
92 | parentId: "notes",
93 | icon: PenTool,
94 | createdAt: new Date(Date.now()),
95 | updatedAt: new Date(Date.now()),
96 | },
97 | {
98 | id: "vocabularies",
99 | name: "单词本",
100 | type: CategoryType.Others,
101 | favorited: false,
102 | archived: false,
103 | parentId: "notes",
104 | icon: BookA,
105 | createdAt: new Date(Date.now()),
106 | updatedAt: new Date(Date.now()),
107 | },
108 | {
109 | id: "terms",
110 | name: "专业术语",
111 | type: CategoryType.Others,
112 | favorited: false,
113 | archived: false,
114 | parentId: "notes",
115 | icon: FileType,
116 | createdAt: new Date(Date.now()),
117 | updatedAt: new Date(Date.now()),
118 | },
119 | {
120 | id: "sentences",
121 | name: "精彩句式",
122 | type: CategoryType.Others,
123 | favorited: false,
124 | archived: false,
125 | parentId: "notes",
126 | icon: ReceiptText,
127 | createdAt: new Date(Date.now()),
128 | updatedAt: new Date(Date.now()),
129 | },
130 | // {
131 | // id: "googlescholar",
132 | // name: "谷歌学术",
133 | // type: CategoryType.Others,
134 | // favorited: false,
135 | // archived: false,
136 | // parentId: "webs",
137 | // icon: GraduationCap,
138 | // createdAt: new Date(Date.now()),
139 | // updatedAt: new Date(Date.now()),
140 | // },
141 | // {
142 | // id: "semanticscholar",
143 | // name: "SemanticScholar",
144 | // type: CategoryType.Others,
145 | // favorited: false,
146 | // archived: false,
147 | // parentId: "webs",
148 | // icon: FolderOpen,
149 | // createdAt: new Date(Date.now()),
150 | // updatedAt: new Date(Date.now()),
151 | // },
152 | // {
153 | // id: "arxiv",
154 | // name: "Arxiv",
155 | // type: CategoryType.Others,
156 | // favorited: false,
157 | // archived: false,
158 | // parentId: "webs",
159 | // icon: ReceiptText,
160 | // createdAt: new Date(Date.now()),
161 | // updatedAt: new Date(Date.now()),
162 | // },
163 | ]
--------------------------------------------------------------------------------
/components/editors/excalidraw-editor.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | import { Excalidraw, MainMenu } from "@excalidraw/excalidraw";
3 | import { Theme } from "@excalidraw/excalidraw/types/element/types";
4 |
5 |
6 | interface ExcalidrawEditorProps {
7 | className: string
8 | initContent: string
9 | onChange: (s: string) => void
10 | }
11 |
12 | const ExcalidrawEditor = ({
13 | className,
14 | initContent,
15 | onChange
16 | }: ExcalidrawEditorProps) => {
17 |
18 | return (
19 |
20 | onChange(JSON.stringify(excalidrawElements))}
26 | UIOptions={{
27 | canvasActions: {
28 | saveToActiveFile: false,
29 | loadScene: false,
30 | export: false,
31 | toggleTheme: false,
32 | changeViewBackgroundColor: false,
33 | },
34 | tools: {
35 | image: false,
36 | },
37 | }}
38 | >
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | )
47 | }
48 |
49 | export default ExcalidrawEditor
--------------------------------------------------------------------------------
/components/editors/icon-picker.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import EmojiPicker, { Theme } from "emoji-picker-react";
4 |
5 | import {
6 | Popover,
7 | PopoverContent,
8 | PopoverTrigger
9 | } from "@/components/ui/popover";
10 |
11 | interface IconPickerProps {
12 | onChange: (icon: string) => void;
13 | children: React.ReactNode;
14 | asChild?: boolean;
15 | };
16 |
17 | export const IconPicker = ({
18 | onChange,
19 | children,
20 | asChild
21 | }: IconPickerProps) => {
22 | const currentTheme = ("light") as keyof typeof themeMap
23 |
24 | const themeMap = {
25 | "dark": Theme.DARK,
26 | "light": Theme.LIGHT
27 | };
28 |
29 | const theme = themeMap[currentTheme];
30 |
31 | return (
32 |
33 |
34 | {children}
35 |
36 |
37 | onChange(data.emoji)}
41 | />
42 |
43 |
44 | );
45 | };
--------------------------------------------------------------------------------
/components/editors/markdown-editor.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | AdmonitionDirectiveDescriptor,
3 | MDXEditor,
4 | toolbarPlugin,
5 | UndoRedo,
6 | BoldItalicUnderlineToggles,
7 | CodeToggle,
8 | CreateLink,
9 | InsertCodeBlock,
10 | InsertImage,
11 | InsertTable,
12 | InsertThematicBreak,
13 | ListsToggle,
14 | codeBlockPlugin,
15 | codeMirrorPlugin,
16 | directivesPlugin,
17 | frontmatterPlugin,
18 | headingsPlugin,
19 | imagePlugin,
20 | linkDialogPlugin,
21 | linkPlugin,
22 | listsPlugin,
23 | markdownShortcutPlugin,
24 | quotePlugin,
25 | tablePlugin,
26 | thematicBreakPlugin,
27 | type CodeBlockEditorDescriptor,
28 | MDXEditorMethods,
29 | CodeMirrorEditor,
30 | InsertAdmonition,
31 | BlockTypeSelect,
32 | Separator,
33 | } from "@mdxeditor/editor"
34 | import "@mdxeditor/editor/style.css"
35 | import axios from "axios"
36 | import { useRef } from 'react'
37 | import { toast } from "sonner"
38 |
39 | type EditorProps = {
40 | key: string,
41 | initContent: string
42 | onChange: (s: string) => void
43 | }
44 |
45 | const FallbackCodeEditorDescriptor: CodeBlockEditorDescriptor = {
46 | match: (language, meta) => true,
47 | priority: 0,
48 | Editor: CodeMirrorEditor
49 | }
50 |
51 | const MarkdownEditor = ({
52 | key,
53 | initContent,
54 | onChange,
55 | }: EditorProps) => {
56 | const editorRef = useRef(null)
57 |
58 | const handleUpload = async (image: File) => {
59 | const formData = new FormData()
60 | formData.append("image", image)
61 | const response = await axios.post("/api/image", formData)
62 | if (response.status !== 200) {
63 | toast.error("Failed to upload image!")
64 | return;
65 | } else {
66 | const url = response.data
67 | toast.success("Image uploaded!")
68 | return url
69 | }
70 | }
71 |
72 | return (
73 | (
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 | ),
129 | }),
130 |
131 | ]}
132 | contentEditableClassName="outline-none max-w-none prose prose-neutral dark:prose-invert caret-black dark:caret-white dark:text-white"
133 | />
134 | )
135 | }
136 |
137 | export default MarkdownEditor
--------------------------------------------------------------------------------
/components/heading.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Link from "next/link";
4 | import { Button } from "./ui/button";
5 | import { ArrowRight } from "lucide-react";
6 |
7 | export const Heading = () => {
8 | return (
9 |
10 |
11 | Welcome to Knowtate
12 |
13 |
14 | Embrace the power of centralized document management, seamless reading, and intelligent QA with AI.
15 |
16 |
17 |
18 | Enter Knowtate
19 |
20 |
21 |
22 |
23 | )
24 | }
--------------------------------------------------------------------------------
/components/heros.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 |
3 | export const Heroes = () => {
4 | return (
5 |
6 |
7 |
8 |
14 |
20 |
21 |
22 |
28 |
34 |
35 |
36 |
37 | )
38 | }
--------------------------------------------------------------------------------
/components/logo.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image"
2 | import Link from "next/link"
3 | import { SiMicrosoftacademic } from "react-icons/si";
4 |
5 |
6 | export const Logo = ({
7 | imageOnly
8 | }: {
9 | imageOnly: boolean
10 | }) => {
11 | return (
12 |
13 |
14 |
15 | {!imageOnly && (
16 |
17 | Knowtate
18 |
19 | )}
20 |
21 |
22 | )
23 | }
--------------------------------------------------------------------------------
/components/navbar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { Logo } from "./logo"
4 |
5 | export const Navbar = () => {
6 |
7 | return (
8 |
9 |
10 |
11 | )
12 | }
--------------------------------------------------------------------------------
/components/search-bar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { Button } from "@/components/ui/button"
4 | import { zodResolver } from "@hookform/resolvers/zod"
5 | import { Dispatch, FormEvent, SetStateAction, useState } from "react"
6 | import { useForm } from "react-hook-form"
7 | import { z } from "zod"
8 | import { Input } from "@/components/ui/input"
9 | import { Loader2, SearchIcon } from "lucide-react"
10 |
11 | const formSchema = z.object({
12 | query: z.string().min(0).max(200),
13 | })
14 |
15 | interface SearchBarProps {
16 | query: string,
17 | setQuery: Dispatch>;
18 | }
19 |
20 | export const SearchBar = ({
21 | query,
22 | setQuery,
23 | }: SearchBarProps) => {
24 | const [input, setInput] = useState("")
25 | const form = useForm>({
26 | resolver: zodResolver(formSchema),
27 | defaultValues: {
28 | query
29 | }
30 | })
31 |
32 | const onSubmit = async (e: FormEvent) => {
33 | e.preventDefault();
34 | setQuery(input)
35 | setInput("")
36 | }
37 |
38 | const handleInputChange = (e: React.ChangeEvent | React.ChangeEvent) => {
39 | setInput(e.target.value);
40 | };
41 |
42 | return (
43 |
67 | )
68 | }
--------------------------------------------------------------------------------
/components/sidebar-item.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 | import { ChevronDown, ChevronUp, LucideIcon } from "lucide-react"
3 | import { Skeleton } from "./ui/skeleton"
4 |
5 | interface SidebarItemProps {
6 | id: string
7 | label: string
8 | icon: LucideIcon
9 | level?: number
10 | active?: boolean
11 | onClick?: () => void
12 | expanded?: boolean
13 | onExpand?: () => void;
14 | showExpandIcon?: boolean
15 | }
16 |
17 | export const SidebarItem = ({
18 | id,
19 | label,
20 | icon: Icon,
21 | level = 0,
22 | active,
23 | onClick,
24 | onExpand,
25 | expanded,
26 | showExpandIcon = false,
27 | }: SidebarItemProps) => {
28 | const handleExpand = (
29 | event: React.MouseEvent
30 | ) => {
31 | event.stopPropagation();
32 | onExpand?.();
33 | };
34 |
35 | const ChevronIcon = expanded ? ChevronUp : ChevronDown;
36 |
37 | return (
38 | <>
39 | {!!id && (
40 |
52 |
59 |
60 | {label}
61 |
62 | {showExpandIcon && (
63 |
68 |
71 |
72 | )}
73 |
74 | )}
75 | >
76 | )
77 | }
78 |
79 | SidebarItem.Skeleton = function ItemSkeleton({ level }: { level?: number }) {
80 | return (
81 |
87 |
88 |
89 |
90 | )
91 | }
--------------------------------------------------------------------------------
/components/sidebar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { CategoryList } from "./category-list"
4 | import { useEffect, useState } from "react"
5 | import axios from "axios"
6 | import { Category } from "@prisma/client"
7 | import { Folder } from "lucide-react"
8 | import { defaultCategories } from "./default-categories"
9 | import { CategoryWithIcon } from "@/types/types"
10 | import { toast } from "sonner"
11 |
12 |
13 | export const Sidebar = () => {
14 | const [categories, setCategories] = useState(defaultCategories)
15 |
16 | useEffect(() => {
17 | const getAllCategories = async () => {
18 | try {
19 | const response = await axios.get("/api/category");
20 | if (response.status === 200) {
21 | const categories: Category[] = response.data;
22 | const updatedCategories = categories.map(category => ({
23 | id: category.id,
24 | name: category.name,
25 | type: category.type,
26 | favorited: category.favorited,
27 | archived: category.archived,
28 | parentId: category.parentId,
29 | icon: Folder,
30 | createdAt: category.createdAt,
31 | updatedAt: category.updatedAt
32 | }));
33 | // 将默认类别和从API获取的类别合并
34 | setCategories([...defaultCategories, ...updatedCategories]);
35 | } else {
36 | toast.error('Failed to get categories');
37 | }
38 | } catch (error) {
39 | console.error('GET CATEGORIES ERROR', error);
40 | toast.error('Failed to get categories');
41 | }
42 | };
43 | // 调用getAllCategories
44 | getAllCategories();
45 | }, [])
46 |
47 | return (
48 |
49 |
54 | {/*
55 |
*/}
60 |
61 | )
62 | }
--------------------------------------------------------------------------------
/components/ui/alert-dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
5 |
6 | import { cn } from "@/lib/utils"
7 | import { buttonVariants } from "@/components/ui/button"
8 |
9 | const AlertDialog = AlertDialogPrimitive.Root
10 |
11 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger
12 |
13 | const AlertDialogPortal = AlertDialogPrimitive.Portal
14 |
15 | const AlertDialogOverlay = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, ...props }, ref) => (
19 |
27 | ))
28 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
29 |
30 | const AlertDialogContent = React.forwardRef<
31 | React.ElementRef,
32 | React.ComponentPropsWithoutRef
33 | >(({ className, ...props }, ref) => (
34 |
35 |
36 |
44 |
45 | ))
46 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
47 |
48 | const AlertDialogHeader = ({
49 | className,
50 | ...props
51 | }: React.HTMLAttributes) => (
52 |
59 | )
60 | AlertDialogHeader.displayName = "AlertDialogHeader"
61 |
62 | const AlertDialogFooter = ({
63 | className,
64 | ...props
65 | }: React.HTMLAttributes) => (
66 |
73 | )
74 | AlertDialogFooter.displayName = "AlertDialogFooter"
75 |
76 | const AlertDialogTitle = React.forwardRef<
77 | React.ElementRef,
78 | React.ComponentPropsWithoutRef
79 | >(({ className, ...props }, ref) => (
80 |
85 | ))
86 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
87 |
88 | const AlertDialogDescription = React.forwardRef<
89 | React.ElementRef,
90 | React.ComponentPropsWithoutRef
91 | >(({ className, ...props }, ref) => (
92 |
97 | ))
98 | AlertDialogDescription.displayName =
99 | AlertDialogPrimitive.Description.displayName
100 |
101 | const AlertDialogAction = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ))
111 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
112 |
113 | const AlertDialogCancel = React.forwardRef<
114 | React.ElementRef,
115 | React.ComponentPropsWithoutRef
116 | >(({ className, ...props }, ref) => (
117 |
126 | ))
127 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
128 |
129 | export {
130 | AlertDialog,
131 | AlertDialogPortal,
132 | AlertDialogOverlay,
133 | AlertDialogTrigger,
134 | AlertDialogContent,
135 | AlertDialogHeader,
136 | AlertDialogFooter,
137 | AlertDialogTitle,
138 | AlertDialogDescription,
139 | AlertDialogAction,
140 | AlertDialogCancel,
141 | }
142 |
--------------------------------------------------------------------------------
/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Avatar = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ))
21 | Avatar.displayName = AvatarPrimitive.Root.displayName
22 |
23 | const AvatarImage = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
32 | ))
33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName
34 |
35 | const AvatarFallback = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 | ))
48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
49 |
50 | export { Avatar, AvatarImage, AvatarFallback }
51 |
--------------------------------------------------------------------------------
/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
13 | destructive:
14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15 | outline:
16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17 | secondary:
18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19 | ghost: "hover:bg-accent hover:text-accent-foreground",
20 | link: "text-primary underline-offset-4 hover:underline",
21 | },
22 | size: {
23 | default: "h-10 px-4 py-2",
24 | sm: "h-9 rounded-md px-3",
25 | lg: "h-11 rounded-md px-8",
26 | icon: "h-10 w-10",
27 | compact: "h-6 w-6"
28 | },
29 | },
30 | defaultVariants: {
31 | variant: "default",
32 | size: "default",
33 | },
34 | }
35 | )
36 |
37 | export interface ButtonProps
38 | extends React.ButtonHTMLAttributes,
39 | VariantProps {
40 | asChild?: boolean
41 | }
42 |
43 | const Button = React.forwardRef(
44 | ({ className, variant, size, asChild = false, ...props }, ref) => {
45 | const Comp = asChild ? Slot : "button"
46 | return (
47 |
52 | )
53 | }
54 | )
55 | Button.displayName = "Button"
56 |
57 | export { Button, buttonVariants }
58 |
--------------------------------------------------------------------------------
/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DialogPrimitive from "@radix-ui/react-dialog"
5 | import { X } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Dialog = DialogPrimitive.Root
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger
12 |
13 | const DialogPortal = DialogPrimitive.Portal
14 |
15 | const DialogClose = DialogPrimitive.Close
16 |
17 | const DialogOverlay = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => (
21 |
29 | ))
30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
31 |
32 | const DialogContent = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, children, ...props }, ref) => (
36 |
37 |
38 |
46 | {children}
47 |
48 |
49 | Close
50 |
51 |
52 |
53 | ))
54 | DialogContent.displayName = DialogPrimitive.Content.displayName
55 |
56 | const DialogHeader = ({
57 | className,
58 | ...props
59 | }: React.HTMLAttributes) => (
60 |
67 | )
68 | DialogHeader.displayName = "DialogHeader"
69 |
70 | const DialogFooter = ({
71 | className,
72 | ...props
73 | }: React.HTMLAttributes) => (
74 |
81 | )
82 | DialogFooter.displayName = "DialogFooter"
83 |
84 | const DialogTitle = React.forwardRef<
85 | React.ElementRef,
86 | React.ComponentPropsWithoutRef
87 | >(({ className, ...props }, ref) => (
88 |
96 | ))
97 | DialogTitle.displayName = DialogPrimitive.Title.displayName
98 |
99 | const DialogDescription = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | DialogDescription.displayName = DialogPrimitive.Description.displayName
110 |
111 | export {
112 | Dialog,
113 | DialogPortal,
114 | DialogOverlay,
115 | DialogClose,
116 | DialogTrigger,
117 | DialogContent,
118 | DialogHeader,
119 | DialogFooter,
120 | DialogTitle,
121 | DialogDescription,
122 | }
123 |
--------------------------------------------------------------------------------
/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
5 | import { Check, ChevronRight, Circle } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const DropdownMenu = DropdownMenuPrimitive.Root
10 |
11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
12 |
13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group
14 |
15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal
16 |
17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub
18 |
19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
20 |
21 | const DropdownMenuSubTrigger = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef & {
24 | inset?: boolean
25 | }
26 | >(({ className, inset, children, ...props }, ref) => (
27 |
36 | {children}
37 |
38 |
39 | ))
40 | DropdownMenuSubTrigger.displayName =
41 | DropdownMenuPrimitive.SubTrigger.displayName
42 |
43 | const DropdownMenuSubContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, ...props }, ref) => (
47 |
55 | ))
56 | DropdownMenuSubContent.displayName =
57 | DropdownMenuPrimitive.SubContent.displayName
58 |
59 | const DropdownMenuContent = React.forwardRef<
60 | React.ElementRef,
61 | React.ComponentPropsWithoutRef
62 | >(({ className, sideOffset = 4, ...props }, ref) => (
63 |
64 |
73 |
74 | ))
75 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
76 |
77 | const DropdownMenuItem = React.forwardRef<
78 | React.ElementRef,
79 | React.ComponentPropsWithoutRef & {
80 | inset?: boolean
81 | }
82 | >(({ className, inset, ...props }, ref) => (
83 |
92 | ))
93 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
94 |
95 | const DropdownMenuCheckboxItem = React.forwardRef<
96 | React.ElementRef,
97 | React.ComponentPropsWithoutRef
98 | >(({ className, children, checked, ...props }, ref) => (
99 |
108 |
109 |
110 |
111 |
112 |
113 | {children}
114 |
115 | ))
116 | DropdownMenuCheckboxItem.displayName =
117 | DropdownMenuPrimitive.CheckboxItem.displayName
118 |
119 | const DropdownMenuRadioItem = React.forwardRef<
120 | React.ElementRef,
121 | React.ComponentPropsWithoutRef
122 | >(({ className, children, ...props }, ref) => (
123 |
131 |
132 |
133 |
134 |
135 |
136 | {children}
137 |
138 | ))
139 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
140 |
141 | const DropdownMenuLabel = React.forwardRef<
142 | React.ElementRef,
143 | React.ComponentPropsWithoutRef & {
144 | inset?: boolean
145 | }
146 | >(({ className, inset, ...props }, ref) => (
147 |
156 | ))
157 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
158 |
159 | const DropdownMenuSeparator = React.forwardRef<
160 | React.ElementRef,
161 | React.ComponentPropsWithoutRef
162 | >(({ className, ...props }, ref) => (
163 |
168 | ))
169 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
170 |
171 | const DropdownMenuShortcut = ({
172 | className,
173 | ...props
174 | }: React.HTMLAttributes) => {
175 | return (
176 |
180 | )
181 | }
182 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
183 |
184 | export {
185 | DropdownMenu,
186 | DropdownMenuTrigger,
187 | DropdownMenuContent,
188 | DropdownMenuItem,
189 | DropdownMenuCheckboxItem,
190 | DropdownMenuRadioItem,
191 | DropdownMenuLabel,
192 | DropdownMenuSeparator,
193 | DropdownMenuShortcut,
194 | DropdownMenuGroup,
195 | DropdownMenuPortal,
196 | DropdownMenuSub,
197 | DropdownMenuSubContent,
198 | DropdownMenuSubTrigger,
199 | DropdownMenuRadioGroup,
200 | }
201 |
--------------------------------------------------------------------------------
/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as LabelPrimitive from "@radix-ui/react-label"
3 | import { Slot } from "@radix-ui/react-slot"
4 | import {
5 | Controller,
6 | ControllerProps,
7 | FieldPath,
8 | FieldValues,
9 | FormProvider,
10 | useFormContext,
11 | } from "react-hook-form"
12 |
13 | import { cn } from "@/lib/utils"
14 | import { Label } from "@/components/ui/label"
15 |
16 | const Form = FormProvider
17 |
18 | type FormFieldContextValue<
19 | TFieldValues extends FieldValues = FieldValues,
20 | TName extends FieldPath = FieldPath
21 | > = {
22 | name: TName
23 | }
24 |
25 | const FormFieldContext = React.createContext(
26 | {} as FormFieldContextValue
27 | )
28 |
29 | const FormField = <
30 | TFieldValues extends FieldValues = FieldValues,
31 | TName extends FieldPath = FieldPath
32 | >({
33 | ...props
34 | }: ControllerProps) => {
35 | return (
36 |
37 |
38 |
39 | )
40 | }
41 |
42 | const useFormField = () => {
43 | const fieldContext = React.useContext(FormFieldContext)
44 | const itemContext = React.useContext(FormItemContext)
45 | const { getFieldState, formState } = useFormContext()
46 |
47 | const fieldState = getFieldState(fieldContext.name, formState)
48 |
49 | if (!fieldContext) {
50 | throw new Error("useFormField should be used within ")
51 | }
52 |
53 | const { id } = itemContext
54 |
55 | return {
56 | id,
57 | name: fieldContext.name,
58 | formItemId: `${id}-form-item`,
59 | formDescriptionId: `${id}-form-item-description`,
60 | formMessageId: `${id}-form-item-message`,
61 | ...fieldState,
62 | }
63 | }
64 |
65 | type FormItemContextValue = {
66 | id: string
67 | }
68 |
69 | const FormItemContext = React.createContext(
70 | {} as FormItemContextValue
71 | )
72 |
73 | const FormItem = React.forwardRef<
74 | HTMLDivElement,
75 | React.HTMLAttributes
76 | >(({ className, ...props }, ref) => {
77 | const id = React.useId()
78 |
79 | return (
80 |
81 |
82 |
83 | )
84 | })
85 | FormItem.displayName = "FormItem"
86 |
87 | const FormLabel = React.forwardRef<
88 | React.ElementRef,
89 | React.ComponentPropsWithoutRef
90 | >(({ className, ...props }, ref) => {
91 | const { error, formItemId } = useFormField()
92 |
93 | return (
94 |
100 | )
101 | })
102 | FormLabel.displayName = "FormLabel"
103 |
104 | const FormControl = React.forwardRef<
105 | React.ElementRef,
106 | React.ComponentPropsWithoutRef
107 | >(({ ...props }, ref) => {
108 | const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
109 |
110 | return (
111 |
122 | )
123 | })
124 | FormControl.displayName = "FormControl"
125 |
126 | const FormDescription = React.forwardRef<
127 | HTMLParagraphElement,
128 | React.HTMLAttributes
129 | >(({ className, ...props }, ref) => {
130 | const { formDescriptionId } = useFormField()
131 |
132 | return (
133 |
139 | )
140 | })
141 | FormDescription.displayName = "FormDescription"
142 |
143 | const FormMessage = React.forwardRef<
144 | HTMLParagraphElement,
145 | React.HTMLAttributes
146 | >(({ className, children, ...props }, ref) => {
147 | const { error, formMessageId } = useFormField()
148 | const body = error ? String(error?.message) : children
149 |
150 | if (!body) {
151 | return null
152 | }
153 |
154 | return (
155 |
161 | {body}
162 |
163 | )
164 | })
165 | FormMessage.displayName = "FormMessage"
166 |
167 | export {
168 | useFormField,
169 | Form,
170 | FormItem,
171 | FormLabel,
172 | FormControl,
173 | FormDescription,
174 | FormMessage,
175 | FormField,
176 | }
177 |
--------------------------------------------------------------------------------
/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = "Input"
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as PopoverPrimitive from "@radix-ui/react-popover"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Popover = PopoverPrimitive.Root
9 |
10 | const PopoverTrigger = PopoverPrimitive.Trigger
11 |
12 | const PopoverContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |
17 |
27 |
28 | ))
29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName
30 |
31 | export { Popover, PopoverTrigger, PopoverContent }
32 |
--------------------------------------------------------------------------------
/components/ui/progress.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ProgressPrimitive from "@radix-ui/react-progress"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Progress = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, value, ...props }, ref) => (
12 |
20 |
24 |
25 | ))
26 | Progress.displayName = ProgressPrimitive.Root.displayName
27 |
28 | export { Progress }
29 |
--------------------------------------------------------------------------------
/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SelectPrimitive from "@radix-ui/react-select"
5 | import { Check, ChevronDown, ChevronUp } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Select = SelectPrimitive.Root
10 |
11 | const SelectGroup = SelectPrimitive.Group
12 |
13 | const SelectValue = SelectPrimitive.Value
14 |
15 | const SelectTrigger = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, children, ...props }, ref) => (
19 | span]:line-clamp-1",
23 | className
24 | )}
25 | {...props}
26 | >
27 | {children}
28 |
29 |
30 |
31 |
32 | ))
33 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
34 |
35 | const SelectScrollUpButton = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 |
48 |
49 | ))
50 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
51 |
52 | const SelectScrollDownButton = React.forwardRef<
53 | React.ElementRef,
54 | React.ComponentPropsWithoutRef
55 | >(({ className, ...props }, ref) => (
56 |
64 |
65 |
66 | ))
67 | SelectScrollDownButton.displayName =
68 | SelectPrimitive.ScrollDownButton.displayName
69 |
70 | const SelectContent = React.forwardRef<
71 | React.ElementRef,
72 | React.ComponentPropsWithoutRef
73 | >(({ className, children, position = "popper", ...props }, ref) => (
74 |
75 |
86 |
87 |
94 | {children}
95 |
96 |
97 |
98 |
99 | ))
100 | SelectContent.displayName = SelectPrimitive.Content.displayName
101 |
102 | const SelectLabel = React.forwardRef<
103 | React.ElementRef,
104 | React.ComponentPropsWithoutRef
105 | >(({ className, ...props }, ref) => (
106 |
111 | ))
112 | SelectLabel.displayName = SelectPrimitive.Label.displayName
113 |
114 | const SelectItem = React.forwardRef<
115 | React.ElementRef,
116 | React.ComponentPropsWithoutRef
117 | >(({ className, children, ...props }, ref) => (
118 |
126 |
127 |
128 |
129 |
130 |
131 |
132 | {children}
133 |
134 | ))
135 | SelectItem.displayName = SelectPrimitive.Item.displayName
136 |
137 | const SelectSeparator = React.forwardRef<
138 | React.ElementRef,
139 | React.ComponentPropsWithoutRef
140 | >(({ className, ...props }, ref) => (
141 |
146 | ))
147 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName
148 |
149 | export {
150 | Select,
151 | SelectGroup,
152 | SelectValue,
153 | SelectTrigger,
154 | SelectContent,
155 | SelectLabel,
156 | SelectItem,
157 | SelectSeparator,
158 | SelectScrollUpButton,
159 | SelectScrollDownButton,
160 | }
161 |
--------------------------------------------------------------------------------
/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SeparatorPrimitive from "@radix-ui/react-separator"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Separator = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(
12 | (
13 | { className, orientation = "horizontal", decorative = true, ...props },
14 | ref
15 | ) => (
16 |
27 | )
28 | )
29 | Separator.displayName = SeparatorPrimitive.Root.displayName
30 |
31 | export { Separator }
32 |
--------------------------------------------------------------------------------
/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 |
3 | function Skeleton({
4 | className,
5 | ...props
6 | }: React.HTMLAttributes) {
7 | return (
8 |
12 | )
13 | }
14 |
15 | export { Skeleton }
16 |
--------------------------------------------------------------------------------
/components/ui/slider.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SliderPrimitive from "@radix-ui/react-slider"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Slider = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
21 |
22 |
23 |
24 |
25 | ))
26 | Slider.displayName = SliderPrimitive.Root.displayName
27 |
28 | export { Slider }
29 |
--------------------------------------------------------------------------------
/components/ui/table.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Table = React.forwardRef<
6 | HTMLTableElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
16 | ))
17 | Table.displayName = "Table"
18 |
19 | const TableHeader = React.forwardRef<
20 | HTMLTableSectionElement,
21 | React.HTMLAttributes
22 | >(({ className, ...props }, ref) => (
23 |
24 | ))
25 | TableHeader.displayName = "TableHeader"
26 |
27 | const TableBody = React.forwardRef<
28 | HTMLTableSectionElement,
29 | React.HTMLAttributes
30 | >(({ className, ...props }, ref) => (
31 |
36 | ))
37 | TableBody.displayName = "TableBody"
38 |
39 | const TableFooter = React.forwardRef<
40 | HTMLTableSectionElement,
41 | React.HTMLAttributes
42 | >(({ className, ...props }, ref) => (
43 | tr]:last:border-b-0",
47 | className
48 | )}
49 | {...props}
50 | />
51 | ))
52 | TableFooter.displayName = "TableFooter"
53 |
54 | const TableRow = React.forwardRef<
55 | HTMLTableRowElement,
56 | React.HTMLAttributes
57 | >(({ className, ...props }, ref) => (
58 |
66 | ))
67 | TableRow.displayName = "TableRow"
68 |
69 | const TableHead = React.forwardRef<
70 | HTMLTableCellElement,
71 | React.ThHTMLAttributes
72 | >(({ className, ...props }, ref) => (
73 |
81 | ))
82 | TableHead.displayName = "TableHead"
83 |
84 | const TableCell = React.forwardRef<
85 | HTMLTableCellElement,
86 | React.TdHTMLAttributes
87 | >(({ className, ...props }, ref) => (
88 |
93 | ))
94 | TableCell.displayName = "TableCell"
95 |
96 | const TableCaption = React.forwardRef<
97 | HTMLTableCaptionElement,
98 | React.HTMLAttributes
99 | >(({ className, ...props }, ref) => (
100 |
105 | ))
106 | TableCaption.displayName = "TableCaption"
107 |
108 | export {
109 | Table,
110 | TableHeader,
111 | TableBody,
112 | TableFooter,
113 | TableHead,
114 | TableRow,
115 | TableCell,
116 | TableCaption,
117 | }
118 |
--------------------------------------------------------------------------------
/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider
9 |
10 | const Tooltip = TooltipPrimitive.Root
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
27 | ))
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
29 |
30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
31 |
--------------------------------------------------------------------------------
/constants.ts:
--------------------------------------------------------------------------------
1 | export const BUCKET_REGION = "local";
2 | export const PAPERS_BUCKET_NAME = "papers";
3 | export const NOTES_BUCKET_NAME = "notes";
4 | export const WHITEBOARDS_BUCKET_NAME = "whiteboards"
5 | export const IMAGES_BUCKET_NAME = "images";
6 | export const PRESIGNED_URL_EXPIRE = 7 * 24 * 60 * 60;
7 | export const DATA_DIR = "storage";
8 | export const INFINITE_QUERY_LIMIT = 10;
9 | export const VECTOR_SEARCH_K = 4;
10 | export const HISTORY_MESSAGE_N = 5;
11 | export const DEMO_USER_ID = "user_2dxgEcETe9zu40VUrTdoOjthEDC"
12 |
--------------------------------------------------------------------------------
/data/category.ts:
--------------------------------------------------------------------------------
1 | import prismadb from "@/lib/prismadb"
2 |
3 |
4 | export const createCategory = async (name: string, type: string, parentId: string) => {
5 | try {
6 | const category = await prismadb.category.create({
7 | data: {
8 | name,
9 | type,
10 | parentId,
11 | favorited: false,
12 | archived: false
13 | }
14 | })
15 | return category
16 | } catch (error) {
17 | console.log("CREATE CATEGORY ERROR", error)
18 | }
19 | }
20 |
21 | export const getCategory = async (id: string) => {
22 | try {
23 | const category = await prismadb.category.findUnique({
24 | where: {
25 | id
26 | }
27 | })
28 | return category
29 | } catch (error) {
30 | console.log("GET CATEGORY ERROR", error)
31 | }
32 | }
33 |
34 | export const renameCategory = async (id: string, name: string) => {
35 | try {
36 | const category = await prismadb.category.update({
37 | where: {
38 | id
39 | },
40 | data: {
41 | name
42 | }
43 | })
44 | return category
45 | } catch (error) {
46 | console.log("GET CATEGORY ERROR", error)
47 | }
48 | }
49 |
50 | export const moveCategory = async (id: string, parentId: string) => {
51 | try {
52 | const category = await prismadb.category.update({
53 | where: {
54 | id
55 | },
56 | data: {
57 | parentId
58 | }
59 | })
60 | return category
61 | } catch (error) {
62 | console.log("MOVE CATEGORY ERROR", error)
63 | }
64 | }
65 |
66 | export const getCategoriesByType = async (type: string) => {
67 | try {
68 | const categories = await prismadb.category.findMany({
69 | where: {
70 | type,
71 | archived: false
72 | }
73 | })
74 | return categories
75 | } catch (error) {
76 | console.log("GET CATEGORIES ERROR", error)
77 | }
78 | }
79 |
80 | export const getCategoriesByParent = async (parentId?: string) => {
81 | try {
82 | const categories = await prismadb.category.findMany({
83 | where: {
84 | parentId: parentId,
85 | archived: false
86 | }
87 | })
88 | return categories
89 | } catch (error) {
90 | console.log("GET CATEGORIES ERROR", error)
91 | }
92 | }
93 |
94 | export const getAllCategories = async () => {
95 | try {
96 | const categories = await prismadb.category.findMany({
97 | where: {
98 | archived: false
99 | }
100 | })
101 | return categories
102 | } catch (error) {
103 | console.log("GET CATEGORIES ERROR", error)
104 | }
105 | }
106 |
107 | export const getArchivedCategories = async () => {
108 | try {
109 | const categories = await prismadb.category.findMany({
110 | where: {
111 | OR: [
112 | { parentId: "library", },
113 | { parentId: "notes", }
114 | ],
115 | archived: true
116 | }
117 | })
118 | return categories
119 | } catch (error) {
120 | console.log("GET CATEGORIES ERROR", error)
121 | }
122 | }
123 |
124 | export const getFavoritedCategories = async () => {
125 | try {
126 | const categories = await prismadb.category.findMany({
127 | where: {
128 | OR: [
129 | { parentId: "library", },
130 | { parentId: "notes", }
131 | ],
132 | archived: false,
133 | favorited: true
134 | }
135 | })
136 | return categories
137 | } catch (error) {
138 | console.log("GET CATEGORIES ERROR", error)
139 | }
140 | }
141 |
142 | export const favoriteCategory = async (id: string, favorited: boolean) => {
143 | try {
144 | const recursiveFavorite = async (categoryId: string) => {
145 | const paperChildren = await prismadb.paper.findMany({
146 | where: {
147 | categoryId: categoryId
148 | }
149 | })
150 | for (const paperChild of paperChildren) {
151 | await prismadb.paper.update({
152 | where: {
153 | id: paperChild.id
154 | },
155 | data: {
156 | favorited: favorited
157 | }
158 | })
159 | }
160 | const noteChildren = await prismadb.note.findMany({
161 | where: {
162 | categoryId: categoryId
163 | }
164 | })
165 | for (const noteChild of noteChildren) {
166 | await prismadb.note.update({
167 | where: {
168 | id: noteChild.id
169 | },
170 | data: {
171 | favorited: favorited
172 | }
173 | })
174 | }
175 | const children = await prismadb.category.findMany({
176 | where: {
177 | parentId: categoryId
178 | }
179 | })
180 | for (const child of children) {
181 | await prismadb.category.update({
182 | where: {
183 | id: child.id
184 | },
185 | data: {
186 | favorited: favorited
187 | }
188 | })
189 | await recursiveFavorite(child.id)
190 | }
191 | }
192 | const category = await prismadb.category.update({
193 | where: {
194 | id
195 | },
196 | data: {
197 | favorited: favorited
198 | }
199 | })
200 | await recursiveFavorite(id)
201 | return category
202 | } catch (error) {
203 | console.log("FAVORITE CATEGORY ERROR", error)
204 | }
205 | }
206 |
207 | export const archiveCategory = async (id: string, archived: boolean) => {
208 | try {
209 | const recursiveArchive = async (categoryId: string) => {
210 | const paperChildren = await prismadb.paper.findMany({
211 | where: {
212 | categoryId: categoryId
213 | }
214 | })
215 | for (const paperChild of paperChildren) {
216 | await prismadb.paper.update({
217 | where: {
218 | id: paperChild.id
219 | },
220 | data: {
221 | archived: true
222 | }
223 | })
224 | }
225 | const noteChildren = await prismadb.note.findMany({
226 | where: {
227 | categoryId: categoryId
228 | }
229 | })
230 | for (const noteChild of noteChildren) {
231 | await prismadb.note.update({
232 | where: {
233 | id: noteChild.id
234 | },
235 | data: {
236 | archived: true
237 | }
238 | })
239 | }
240 | const children = await prismadb.category.findMany({
241 | where: {
242 | parentId: categoryId
243 | }
244 | })
245 | for (const child of children) {
246 | await prismadb.category.update({
247 | where: {
248 | id: child.id
249 | },
250 | data: {
251 | archived: true
252 | }
253 | })
254 | await recursiveArchive(child.id)
255 | }
256 | }
257 | const archivedCategory = await prismadb.category.update({
258 | where: {
259 | id
260 | },
261 | data: {
262 | archived: true
263 | }
264 | })
265 | await recursiveArchive(id)
266 | return archivedCategory
267 | } catch (error) {
268 | console.log("ARCHIVE CATEGORY ERROR", error)
269 | }
270 | }
271 |
272 | export const removeCategory = async (id: string) => {
273 | try {
274 | const recursiveRemove = async (categoryId: string) => {
275 | const paperChildren = await prismadb.paper.findMany({
276 | where: {
277 | categoryId: categoryId
278 | }
279 | })
280 | for (const paperChild of paperChildren) {
281 | await prismadb.paper.delete({
282 | where: {
283 | id: paperChild.id
284 | },
285 | })
286 | }
287 | const noteChildren = await prismadb.note.findMany({
288 | where: {
289 | categoryId: categoryId
290 | }
291 | })
292 | for (const noteChild of noteChildren) {
293 | await prismadb.note.delete({
294 | where: {
295 | id: noteChild.id
296 | },
297 | })
298 | }
299 | const children = await prismadb.category.findMany({
300 | where: {
301 | parentId: categoryId
302 | }
303 | })
304 | for (const child of children) {
305 | await prismadb.category.delete({
306 | where: {
307 | id: child.id
308 | }
309 | })
310 | await recursiveRemove(child.id)
311 | }
312 | }
313 | const removedCategory = await prismadb.category.delete({
314 | where: {
315 | id
316 | },
317 | })
318 | await recursiveRemove(id)
319 | return removedCategory
320 | } catch (error) {
321 | console.log("REMOVE CATEGORY ERROR", error)
322 | }
323 | }
--------------------------------------------------------------------------------
/data/message.ts:
--------------------------------------------------------------------------------
1 | import prismadb from "@/lib/prismadb"
2 |
3 |
4 | export const createMessage = async (paperId: string, role: string, content: string) => {
5 | try {
6 | const message = await prismadb.message.create({
7 | data: {
8 | paperId,
9 | role,
10 | content
11 | }
12 | })
13 | return message
14 | } catch (error) {
15 | console.log("CREATE MESSAGE ERROR", error)
16 | }
17 | }
18 |
19 | export const getMessages = async (paperId: string) => {
20 | try {
21 | const messages = await prismadb.message.findMany({
22 | where: {
23 | paperId
24 | },
25 | orderBy: {
26 | createdAt: "asc"
27 | }
28 | })
29 | return messages
30 | } catch (error) {
31 | console.log("GET MESSAGES ERROR", error)
32 | }
33 | }
34 |
35 | export const getLimitedMessages = async (paperId: string, limit: number) => {
36 | try {
37 | const messages = await prismadb.message.findMany({
38 | where: {
39 | paperId
40 | },
41 | orderBy: {
42 | createdAt: "desc"
43 | },
44 | take: limit
45 | })
46 | return messages
47 | } catch (error) {
48 | console.log("GET LIMITED MESSAGES ERROR", error)
49 | }
50 | }
51 |
52 | export const removeMessagesOfPaper = async (paperId: string) => {
53 | try {
54 | const messages = await prismadb.message.deleteMany({
55 | where: {
56 | paperId
57 | }
58 | })
59 | return messages
60 | } catch (error) {
61 | console.log("REMOVE MESSAGES ERROR", error)
62 | }
63 | }
--------------------------------------------------------------------------------
/data/note.ts:
--------------------------------------------------------------------------------
1 | import prismadb from "@/lib/prismadb"
2 |
3 |
4 | export const createNote = async (title: string, type: string, categoryId: string, paperId?: string) => {
5 | try {
6 | const note = await prismadb.note.create({
7 | data: {
8 | title,
9 | paperId,
10 | type,
11 | categoryId,
12 | content: "",
13 | favorited: false,
14 | archived: false
15 | }
16 | })
17 | return note
18 | } catch (error) {
19 | console.log("CREATE NOTE ERROR", error)
20 | }
21 | }
22 |
23 | export const getNotes = async (categoryId?: string) => {
24 | try {
25 | const allNotes = await prismadb.note.findMany({
26 | where: {
27 | categoryId,
28 | archived: false
29 | },
30 | })
31 | return allNotes
32 | } catch (error) {
33 | console.log("GET NOTES ERROR", error)
34 | }
35 | }
36 |
37 | export const getArchivedNotesWithPaper = async () => {
38 | try {
39 | const allNotes = await prismadb.note.findMany({
40 | where: {
41 | OR: [
42 | { categoryId: "notes" },
43 | { categoryId: "whiteboards" }
44 | ],
45 | archived: true
46 | },
47 | include: {
48 | paper: true
49 | }
50 | })
51 | return allNotes
52 | } catch (error) {
53 | console.log("GET NOTES ERROR", error)
54 | }
55 | }
56 |
57 | export const getFavoritedNotesWithPaper = async () => {
58 | try {
59 | const allNotes = await prismadb.note.findMany({
60 | where: {
61 | OR: [
62 | { categoryId: "notes" },
63 | { categoryId: "whiteboards" }
64 | ],
65 | archived: false,
66 | favorited: true
67 | },
68 | include: {
69 | paper: true
70 | }
71 | })
72 | return allNotes
73 | } catch (error) {
74 | console.log("GET NOTES ERROR", error)
75 | }
76 | }
77 |
78 | export const getNotesofPaper = async (paperId?: string) => {
79 | try {
80 | const allNotes = await prismadb.note.findMany({
81 | where: {
82 | paperId,
83 | archived: false
84 | },
85 | })
86 | return allNotes
87 | } catch (error) {
88 | console.log("GET NOTES ERROR", error)
89 | }
90 | }
91 |
92 | export const getNotesWithPaper = async (categoryId?: string) => {
93 | try {
94 | const allNotes = await prismadb.note.findMany({
95 | where: {
96 | categoryId,
97 | archived: false
98 | },
99 | include: {
100 | paper: true
101 | }
102 | })
103 | return allNotes
104 | } catch (error) {
105 | console.log("GET NOTES ERROR", error)
106 | }
107 | }
108 |
109 | export const getNoteById = async (id: string) => {
110 | try {
111 | const note = await prismadb.note.findUnique({
112 | where: {
113 | id
114 | }
115 | })
116 | return note
117 | } catch (error) {
118 | console.log("GET NOTE BY ID ERROR", error)
119 | }
120 | }
121 |
122 | export const getNoteWithPaper = async (id: string) => {
123 | try {
124 | const note = await prismadb.note.findUnique({
125 | where: {
126 | id
127 | },
128 | include: {
129 | paper: true
130 | }
131 | })
132 | return note
133 | } catch (error) {
134 | console.log("GET NOTE BY ID ERROR", error)
135 | }
136 | }
137 |
138 | export const getNoteOfPaper = async (paperId: string) => {
139 | try {
140 | const notes = await prismadb.note.findMany({
141 | where: {
142 | paperId
143 | }
144 | })
145 | return notes
146 | } catch (error) {
147 | console.log("GET NOTE OF PAPER ERROR", error)
148 | }
149 | }
150 |
151 | export const favoriteNote = async (id: string, favorited: boolean) => {
152 | try {
153 | const note = await prismadb.note.update({
154 | where: {
155 | id
156 | },
157 | data: {
158 | favorited
159 | }
160 | })
161 | return note
162 | } catch (error) {
163 | console.log("FAVORITE NOTE ERROR", error)
164 | }
165 | }
166 |
167 | export const archiveNote = async (id: string, archived: boolean) => {
168 | try {
169 | const note = await prismadb.note.update({
170 | where: {
171 | id
172 | },
173 | data: {
174 | archived
175 | }
176 | })
177 | return note
178 | } catch (error) {
179 | console.log("ARCHIVE NOTE ERROR", error)
180 | }
181 | }
182 |
183 | export const renameNote = async (id: string, title: string) => {
184 | try {
185 | const note = await prismadb.note.update({
186 | where: {
187 | id
188 | },
189 | data: {
190 | title
191 | }
192 | })
193 | return note
194 | } catch (error) {
195 | console.log("RENAME Note ERROR", error)
196 | }
197 | }
198 |
199 | export const removeNote = async (id: string) => {
200 | try {
201 | const note = await prismadb.note.delete({
202 | where: {
203 | id
204 | }
205 | })
206 | return note
207 | } catch (error) {
208 | console.log("REMOVE NOTE ERROR", error)
209 | }
210 | }
211 |
212 | export const removeNotesOfPaper = async (paperId: string) => {
213 | try {
214 | const notes = await prismadb.note.deleteMany({
215 | where: {
216 | paperId
217 | }
218 | })
219 | return notes
220 | } catch (error) {
221 | console.log("REMOVE NOTES OF PAPER ERROR", error)
222 | }
223 | }
224 |
225 | export const updateContent = async (id: string, content: string) => {
226 | try {
227 | const note = await prismadb.note.update({
228 | where: {
229 | id
230 | },
231 | data: {
232 | content
233 | }
234 | })
235 | return note
236 | } catch (error) {
237 | console.log("UPDATE CONTENT OF NOTE ERROR", error)
238 | }
239 | }
240 |
241 | export const updateIcon = async (id: string, icon: string) => {
242 | try {
243 | const note = await prismadb.note.update({
244 | where: {
245 | id
246 | },
247 | data: {
248 | icon
249 | }
250 | })
251 | return note
252 | } catch (error) {
253 | console.log("UPDATE ICON OF NOTE ERROR", error)
254 | }
255 | }
--------------------------------------------------------------------------------
/data/paper.ts:
--------------------------------------------------------------------------------
1 | import prismadb from "@/lib/prismadb"
2 |
3 |
4 | export const createPaper = async (
5 | title: string,
6 | url: string,
7 | categoryId: string,
8 | authors?: string,
9 | publication?: string,
10 | publicateDate?: string,
11 | ) => {
12 | try {
13 | const paper = await prismadb.paper.create({
14 | data: {
15 | title,
16 | url,
17 | categoryId,
18 | authors,
19 | publication,
20 | publicateDate,
21 | favorited: false,
22 | archived: false
23 | }
24 | })
25 | return paper
26 | } catch (error) {
27 | console.log("CREATE PAPER ERROR", error)
28 | }
29 | }
30 |
31 | export const getPapers = async (categoryId?: string) => {
32 | try {
33 | const papers = await prismadb.paper.findMany({
34 | where: {
35 | categoryId,
36 | archived: false
37 | }
38 | })
39 | return papers
40 | } catch (error) {
41 | console.log("GET Papers ERROR", error)
42 | }
43 | }
44 |
45 | export const getArchivedPapers = async () => {
46 | try {
47 | const papers = await prismadb.paper.findMany({
48 | where: {
49 | categoryId: "library",
50 | archived: true
51 | }
52 | })
53 | return papers
54 | } catch (error) {
55 | console.log("GET Papers ERROR", error)
56 | }
57 | }
58 |
59 | export const getFavoritedPapers = async () => {
60 | try {
61 | const papers = await prismadb.paper.findMany({
62 | where: {
63 | categoryId: "library",
64 | archived: false,
65 | favorited: true
66 | }
67 | })
68 | return papers
69 | } catch (error) {
70 | console.log("GET Papers ERROR", error)
71 | }
72 | }
73 |
74 | export const getRecentPapers = async () => {
75 | try {
76 | const today = new Date();
77 | const lastMonthDate = new Date(today);
78 | lastMonthDate.setMonth(today.getMonth() - 1);
79 | const papers = await prismadb.paper.findMany({
80 | where: {
81 | archived: false,
82 | updatedAt: {
83 | gte: lastMonthDate
84 | }
85 | }
86 | })
87 | return papers
88 | } catch (error) {
89 | console.log("GET RECENT PAPERS ERROR", error)
90 | }
91 | }
92 |
93 | export const getPaperById = async (id: string) => {
94 | try {
95 | const paper = await prismadb.paper.findUnique({
96 | where: {
97 | id
98 | }
99 | })
100 | return paper
101 | } catch (error) {
102 | console.log("GET PAPER ERROR", error)
103 | }
104 | }
105 |
106 | export const getPaperWithNotes = async (id: string) => {
107 | try {
108 | const paper = await prismadb.paper.findUnique({
109 | where: {
110 | id
111 | },
112 | include: {
113 | notes: true,
114 | }
115 | })
116 | return paper
117 | } catch (error) {
118 | console.log("GET PAPER ERROR", error)
119 | }
120 | }
121 |
122 | export const getPaperWithNotesAndMessages = async (id: string) => {
123 | try {
124 | const paper = await prismadb.paper.findUnique({
125 | where: {
126 | id
127 | },
128 | include: {
129 | notes: true,
130 | messages: true
131 | }
132 | })
133 | return paper
134 | } catch (error) {
135 | console.log("GET PAPER ERROR", error)
136 | }
137 | }
138 |
139 | export const favoritePaper = async (id: string, favorited: boolean) => {
140 | try {
141 | const paper = await prismadb.paper.update({
142 | where: {
143 | id
144 | },
145 | data: {
146 | favorited
147 | }
148 | })
149 | return paper
150 | } catch (error) {
151 | console.log("FAVORITE PAPER ERROR", error)
152 | }
153 | }
154 |
155 | export const archivePaper = async (id: string, archived: boolean) => {
156 | try {
157 | const paper = await prismadb.paper.update({
158 | where: {
159 | id
160 | },
161 | data: {
162 | archived
163 | }
164 | })
165 | return paper
166 | } catch (error) {
167 | console.log("ARCHIVE PAPER ERROR", error)
168 | }
169 | }
170 |
171 | export const renamePaper = async (id: string, title: string) => {
172 | try {
173 | const paper = await prismadb.paper.update({
174 | where: {
175 | id
176 | },
177 | data: {
178 | title
179 | }
180 | })
181 | return paper
182 | } catch (error) {
183 | console.log("RENAME PAPER ERROR", error)
184 | }
185 | }
186 |
187 | export const movePaper = async (id: string, parentId: string) => {
188 | try {
189 | const paper = await prismadb.paper.update({
190 | where: {
191 | id
192 | },
193 | data: {
194 | categoryId: parentId
195 | }
196 | })
197 | return paper
198 | } catch (error) {
199 | console.log("MOVE PAPER ERROR", error)
200 | }
201 | }
202 |
203 | export const removePaper = async (id: string) => {
204 | try {
205 | const paper = await prismadb.paper.delete({
206 | where: {
207 | id
208 | }
209 | })
210 | return paper
211 | } catch (error) {
212 | console.log("REMOVE PAPER ERROR", error)
213 | }
214 | }
215 |
216 | export const updateAnnotionOfPaper = async (id: string, annotations: string) => {
217 | try {
218 | const paper = await prismadb.paper.update({
219 | where: {
220 | id
221 | },
222 | data: {
223 | annotations
224 | }
225 | })
226 | return paper
227 | } catch (error) {
228 | console.log("UPDATE ANNOTATIONS OF PAPER ERROR", error)
229 | }
230 | }
--------------------------------------------------------------------------------
/hooks/use-annotations.tsx:
--------------------------------------------------------------------------------
1 | import { updateAnnotionOfPaper } from "@/data/paper"
2 | import { Annotation } from "@/types/types"
3 | import { Paper } from "@prisma/client"
4 | import { useEffect, useState } from "react"
5 |
6 |
7 | export const useAnnotations = (paper: Paper) => {
8 | const [annotations, setAnnotations] = useState([])
9 |
10 | useEffect(() => {
11 | if (paper.annotations) {
12 | try {
13 | const initAnnotations: Annotation[] = JSON.parse(paper.annotations)
14 | setAnnotations(initAnnotations)
15 | } catch (error) {
16 | console.log("Failed to parse annotations", error)
17 | setAnnotations([])
18 | }
19 | }
20 | }, [paper])
21 |
22 | useEffect(() => {
23 | if (annotations.length > 0) {
24 | updateAnnotionOfPaper(paper.id, JSON.stringify(annotations)).catch(error => {
25 | console.log("Failed to update annotations", error)
26 | })
27 | }
28 | }, [annotations, paper])
29 | return [annotations, setAnnotations] as const
30 | }
--------------------------------------------------------------------------------
/lib/prismadb.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client";
2 | import path from 'path';
3 |
4 | declare global {
5 | var prisma: PrismaClient | undefined;
6 | }
7 |
8 | const filePath = path.join(process.cwd(), 'prisma/researcher.db')
9 | const prismadb = globalThis.prisma || new PrismaClient({
10 | datasources: {
11 | db: {
12 | // url: process.env.DATABASE_URL,
13 | url: 'file:' + filePath,
14 | },
15 | },
16 | });
17 | if (process.env.NODE_ENV !== "production") globalThis.prisma = prismadb;
18 |
19 | export default prismadb;
20 |
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | experimental: {
4 | serverComponentsExternalPackages: ["pdf2json"],
5 | },
6 | transpilePackages: ['@mdxeditor/editor'],
7 | reactStrictMode: true,
8 | webpack: (config, { webpack }) => {
9 | config.experiments = {
10 | ...config.experiments,
11 | topLevelAwait: true,
12 | };
13 | config.externals.push({
14 | sharp: "commonjs sharp",
15 | canvas: "commonjs canvas",
16 | topLevelAwait: true
17 | });
18 | // 添加别名解析
19 | return config;
20 | }
21 | };
22 |
23 | export default nextConfig;
24 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "knowtate",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "db:reset": "npx prisma db push --force-reset",
7 | "db:recreate": "npx prisma db push --force-reset && npx prisma db push",
8 | "db:seed": "npx prisma db seed",
9 | "db:reseed": "npx prisma db push --force-reset && npx prisma db seed",
10 | "postinstall": "prisma generate && npx prisma db push --force-reset && npx prisma db push",
11 | "dev": "next dev",
12 | "build": "next build",
13 | "start": "next start",
14 | "lint": "next lint"
15 | },
16 | "dependencies": {
17 | "@excalidraw/excalidraw": "^0.17.3",
18 | "@hookform/resolvers": "^3.3.4",
19 | "@langchain/community": "^0.0.48",
20 | "@mdxeditor/editor": "^2.20.0",
21 | "@prisma/client": "^5.12.1",
22 | "@radix-ui/react-alert-dialog": "^1.0.5",
23 | "@radix-ui/react-avatar": "^1.0.4",
24 | "@radix-ui/react-dialog": "^1.0.5",
25 | "@radix-ui/react-dropdown-menu": "^2.0.6",
26 | "@radix-ui/react-label": "^2.0.2",
27 | "@radix-ui/react-popover": "^1.0.7",
28 | "@radix-ui/react-progress": "^1.0.3",
29 | "@radix-ui/react-select": "^2.0.0",
30 | "@radix-ui/react-separator": "^1.0.3",
31 | "@radix-ui/react-slider": "^1.1.2",
32 | "@radix-ui/react-slot": "^1.0.2",
33 | "@radix-ui/react-tooltip": "^1.0.7",
34 | "@react-pdf-viewer/bookmark": "^3.12.0",
35 | "@react-pdf-viewer/core": "^3.12.0",
36 | "@react-pdf-viewer/default-layout": "^3.12.0",
37 | "@react-pdf-viewer/highlight": "^3.12.0",
38 | "@react-pdf-viewer/thumbnail": "^3.12.0",
39 | "@react-pdf-viewer/toolbar": "^3.12.0",
40 | "@tailwindcss/typography": "^0.5.12",
41 | "@tanstack/react-table": "^8.15.3",
42 | "ai": "^3.0.22",
43 | "axios": "^1.6.8",
44 | "canvas": "^2.11.2",
45 | "class-variance-authority": "^0.7.0",
46 | "clsx": "^2.1.0",
47 | "date-fns": "^3.6.0",
48 | "emoji-picker-react": "^4.9.2",
49 | "langchain": "^0.1.33",
50 | "lucide-react": "^0.367.0",
51 | "next": "14.1.4",
52 | "openai": "^4.33.1",
53 | "pdf-parse": "^1.1.1",
54 | "pdf2json": "^3.0.5",
55 | "pdfjs-dist": "^3.4.120",
56 | "react": "^18",
57 | "react-dom": "^18",
58 | "react-dropzone": "^14.2.3",
59 | "react-hook-form": "^7.51.2",
60 | "react-icons": "^5.0.1",
61 | "react-markdown": "^9.0.1",
62 | "react-spinners": "^0.13.8",
63 | "react-textarea-autosize": "^8.5.3",
64 | "rss-parser": "^3.13.0",
65 | "sonner": "^1.4.41",
66 | "tailwind-merge": "^2.2.2",
67 | "tailwindcss-animate": "^1.0.7",
68 | "uuid": "^9.0.1",
69 | "zod": "^3.22.4"
70 | },
71 | "devDependencies": {
72 | "@types/node": "^20",
73 | "@types/react": "^18",
74 | "@types/react-dom": "^18",
75 | "@types/uuid": "^9.0.8",
76 | "autoprefixer": "^10.0.1",
77 | "eslint": "^8",
78 | "eslint-config-next": "14.1.4",
79 | "postcss": "^8",
80 | "prisma": "^5.12.1",
81 | "tailwindcss": "^3.3.0",
82 | "typescript": "^5"
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/prisma/researcher.db:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tsmotlp/knowtate/fe55070afb0379468895d5ca2866bf5cb99a8bc6/prisma/researcher.db
--------------------------------------------------------------------------------
/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | // This is your Prisma schema file,
2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema
3 |
4 | // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
5 | // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
6 |
7 | generator client {
8 | provider = "prisma-client-js"
9 | }
10 |
11 | datasource db {
12 | provider = "sqlite"
13 | url = env("DATABASE_URL")
14 | }
15 |
16 | model Category {
17 | id String @id @default(uuid())
18 | name String
19 | type String
20 | parentId String
21 | favorited Boolean
22 | archived Boolean
23 | createdAt DateTime @default(now())
24 | updatedAt DateTime @updatedAt
25 | }
26 |
27 | model Paper {
28 | id String @id @default(uuid())
29 | title String
30 | authors String?
31 | publication String?
32 | publicateDate String?
33 | importance String?
34 | url String
35 | categoryId String
36 | favorited Boolean
37 | archived Boolean
38 | annotations String?
39 | messages Message[]
40 | notes Note[]
41 | createdAt DateTime @default(now())
42 | updatedAt DateTime @updatedAt
43 | }
44 |
45 | model Note {
46 | id String @id @default(uuid())
47 | title String
48 | type String
49 | icon String?
50 | content String?
51 | favorited Boolean
52 | archived Boolean
53 | paper Paper? @relation(fields: [paperId], references: [id])
54 | paperId String?
55 | categoryId String
56 | createdAt DateTime @default(now())
57 | updatedAt DateTime @updatedAt
58 | }
59 |
60 | model Message {
61 | id String @id @default(uuid())
62 | role String
63 | paper Paper @relation(fields: [paperId], references: [id])
64 | paperId String
65 | content String
66 | createdAt DateTime @default(now())
67 | updatedAt DateTime @updatedAt
68 | }
69 |
--------------------------------------------------------------------------------
/public/bot.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/dashboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tsmotlp/knowtate/fe55070afb0379468895d5ca2866bf5cb99a8bc6/public/dashboard.png
--------------------------------------------------------------------------------
/public/documents-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tsmotlp/knowtate/fe55070afb0379468895d5ca2866bf5cb99a8bc6/public/documents-dark.png
--------------------------------------------------------------------------------
/public/documents.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tsmotlp/knowtate/fe55070afb0379468895d5ca2866bf5cb99a8bc6/public/documents.png
--------------------------------------------------------------------------------
/public/empty-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tsmotlp/knowtate/fe55070afb0379468895d5ca2866bf5cb99a8bc6/public/empty-dark.png
--------------------------------------------------------------------------------
/public/empty.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tsmotlp/knowtate/fe55070afb0379468895d5ca2866bf5cb99a8bc6/public/empty.png
--------------------------------------------------------------------------------
/public/error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tsmotlp/knowtate/fe55070afb0379468895d5ca2866bf5cb99a8bc6/public/error.png
--------------------------------------------------------------------------------
/public/files.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tsmotlp/knowtate/fe55070afb0379468895d5ca2866bf5cb99a8bc6/public/files.png
--------------------------------------------------------------------------------
/public/paper-reading.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tsmotlp/knowtate/fe55070afb0379468895d5ca2866bf5cb99a8bc6/public/paper-reading.png
--------------------------------------------------------------------------------
/public/papers/1718771923339.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tsmotlp/knowtate/fe55070afb0379468895d5ca2866bf5cb99a8bc6/public/papers/1718771923339.pdf
--------------------------------------------------------------------------------
/public/reading-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tsmotlp/knowtate/fe55070afb0379468895d5ca2866bf5cb99a8bc6/public/reading-dark.png
--------------------------------------------------------------------------------
/public/reading.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tsmotlp/knowtate/fe55070afb0379468895d5ca2866bf5cb99a8bc6/public/reading.png
--------------------------------------------------------------------------------
/public/user.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss"
2 |
3 | const config = {
4 | darkMode: ["class"],
5 | content: [
6 | './pages/**/*.{ts,tsx}',
7 | './components/**/*.{ts,tsx}',
8 | './app/**/*.{ts,tsx}',
9 | './src/**/*.{ts,tsx}',
10 | ],
11 | prefix: "",
12 | theme: {
13 | container: {
14 | center: true,
15 | padding: "2rem",
16 | screens: {
17 | "2xl": "1400px",
18 | },
19 | },
20 | extend: {
21 | colors: {
22 | border: "hsl(var(--border))",
23 | input: "hsl(var(--input))",
24 | ring: "hsl(var(--ring))",
25 | background: "hsl(var(--background))",
26 | foreground: "hsl(var(--foreground))",
27 | primary: {
28 | DEFAULT: "hsl(var(--primary))",
29 | foreground: "hsl(var(--primary-foreground))",
30 | },
31 | secondary: {
32 | DEFAULT: "hsl(var(--secondary))",
33 | foreground: "hsl(var(--secondary-foreground))",
34 | },
35 | destructive: {
36 | DEFAULT: "hsl(var(--destructive))",
37 | foreground: "hsl(var(--destructive-foreground))",
38 | },
39 | muted: {
40 | DEFAULT: "hsl(var(--muted))",
41 | foreground: "hsl(var(--muted-foreground))",
42 | },
43 | accent: {
44 | DEFAULT: "hsl(var(--accent))",
45 | foreground: "hsl(var(--accent-foreground))",
46 | },
47 | popover: {
48 | DEFAULT: "hsl(var(--popover))",
49 | foreground: "hsl(var(--popover-foreground))",
50 | },
51 | card: {
52 | DEFAULT: "hsl(var(--card))",
53 | foreground: "hsl(var(--card-foreground))",
54 | },
55 | },
56 | borderRadius: {
57 | lg: "var(--radius)",
58 | md: "calc(var(--radius) - 2px)",
59 | sm: "calc(var(--radius) - 4px)",
60 | },
61 | keyframes: {
62 | "accordion-down": {
63 | from: { height: "0" },
64 | to: { height: "var(--radix-accordion-content-height)" },
65 | },
66 | "accordion-up": {
67 | from: { height: "var(--radix-accordion-content-height)" },
68 | to: { height: "0" },
69 | },
70 | },
71 | animation: {
72 | "accordion-down": "accordion-down 0.2s ease-out",
73 | "accordion-up": "accordion-up 0.2s ease-out",
74 | },
75 | },
76 | },
77 | plugins: [
78 | require("tailwindcss-animate"),
79 | require('@tailwindcss/typography'),
80 | ],
81 | } satisfies Config
82 |
83 | export default config
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./*"]
22 | }
23 | },
24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25 | "exclude": ["node_modules"]
26 | }
27 |
--------------------------------------------------------------------------------
/types/types.tsx:
--------------------------------------------------------------------------------
1 | import { Category, Note, Paper } from "@prisma/client"
2 | import { LucideIcon } from "lucide-react"
3 | import { HighlightArea } from "@react-pdf-viewer/highlight"
4 |
5 | export enum AnnotationType {
6 | Highlight = "Highlight",
7 | Underline = "Underline"
8 | }
9 |
10 | export type Annotation = {
11 | id: string
12 | type: AnnotationType
13 | color: string
14 | opacity: number
15 | popoverTop: string
16 | popoverLeft: string
17 | areas: HighlightArea[]
18 | }
19 |
20 | export type CategoryWithIcon = Category & {
21 | icon: LucideIcon
22 | }
23 |
24 | export type PaperMenuType = {
25 | info: boolean,
26 | notes: boolean,
27 | chat: boolean
28 | }
29 |
30 | export type NoteWithPaper = Note & {
31 | paper: Paper | null
32 | }
33 |
34 | export type PaperWithNotes = Paper & {
35 | notes: Note[],
36 | }
37 |
38 | export enum CategoryType {
39 | Papers = "Paper",
40 | Markdowns = "Markdowns",
41 | Whiteboards = "Whiteboards",
42 | Others = "Others"
43 | }
44 |
45 | export enum DashboardItemType {
46 | Category = "Category",
47 | Paper = "Paper",
48 | Markdown = "Markdown",
49 | Whiteboard = "Whiteboard"
50 | }
51 |
52 | export type DashboardItem = {
53 | id: string,
54 | label: string,
55 | type: DashboardItemType,
56 | favorited: boolean,
57 | archived: boolean,
58 | url: string | null,
59 | authors: string | null,
60 | publication: string | null,
61 | publicationDate: string | null,
62 | paperTile: string | null,
63 | paperId?: string | null,
64 | lastEdit: Date,
65 | }
--------------------------------------------------------------------------------