diff --git a/@types/next-auth.d.ts b/@types/next-auth.d.ts deleted file mode 100644 index 34c940e..0000000 --- a/@types/next-auth.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -import "next-auth"; - -// next-auth.d.ts -import NextAuth from "next-auth"; - -declare module "next-auth" { - interface Session { - user: { - id?: string - name?: string | null - email?: string | null - image?: string | null - }; - } - interface User { - id?: string - name?: string | null - email?: string | null - image?: string | null - - } - interface JWT { - id?: string; - } - -} diff --git a/Clients/RAG_Client.ts b/Clients/RAG_Client.ts new file mode 100644 index 0000000..273e738 --- /dev/null +++ b/Clients/RAG_Client.ts @@ -0,0 +1,165 @@ +/* ======================= + Response Types +======================= */ +interface StoreResponse { + file_name: string, + file_type: string, + job_id: string, + stage: string, + status: string, +} + +interface QueryResponse { + text: string + score: number + token_count: number + content_type: string + metadata: Record + table_json: (string | null)[][] + reference: { + file: string + section: string + pages: number[] + } +} + +/* ======================= + Core Client +======================= */ + +class RAGClient { + private baseUrl: string + + constructor(baseUrl: string) { + this.baseUrl = baseUrl + } + + /* ----------------------- + STORE + ----------------------- */ + + store = { + file: { + all: (file: File, metadata: any) => + this.uploadFile("/store/upload/all", file, metadata), + pdf: (file: File, metadata?: any) => + this.uploadFile("/store/upload/pdf", file, metadata), + + md: (file: File, metadata?: any) => + this.uploadFile("/store/upload/md", file, metadata), + + csv: (file: File, metadata?: any) => + this.uploadFile("/store/upload/csv", file, metadata), + + json: (file: File, metadata?: any) => + this.uploadFile("/store/upload/json", file, metadata), + + sheet: (file: File, metadata?: any) => + this.uploadFile("/store/upload/sheet", file, metadata) + }, + + url: { + all: (url: string, metadata?: any) => + this.uploadUrl("/store/url/all", url, metadata), + pdf: (url: string, metadata?: any) => + this.uploadUrl("/store/url/pdf", url, metadata), + + md: (url: string, metadata?: any) => + this.uploadUrl("/store/url/md", url, metadata), + + csv: (url: string, metadata?: any) => + this.uploadUrl("/store/url/csv", url, metadata), + + json: (url: string, metadata?: any) => + this.uploadUrl("/store/url/json", url, metadata), + + sheet: (url: string, metadata?: any) => + this.uploadUrl("/store/url/sheet", url, metadata) + } + } + + getProgressUrl(userId: string) { + return `${this.baseUrl}/progress/${userId}`; + } + /* ----------------------- + QUERY + ----------------------- */ + + async query( + queries: string[], + opts: { + metadata: string + top_result?: number | null + } + ): Promise { + const body = new URLSearchParams() + + queries.forEach(q => body.append("queries", q)) + if (opts.metadata) body.append("metadata", opts.metadata) + if (opts?.top_result != null) + body.append("top_result", String(opts.top_result)) + + return this.request("/query", { + method: "POST", + body + }) + } + + /* ======================= + Internal helpers +======================= */ + + private async uploadFile( + path: string, + file: File, + metadata: any + ): Promise { + const form = new FormData() + form.append("upload", file) + form.append("metadata", JSON.stringify(metadata)) + + return this.request(path, { + method: "POST", + body: form + }) + } + + private async uploadUrl( + path: string, + url: string, + metadata: any + ): Promise { + const body = new URLSearchParams() + body.append("url", url) + body.append("metadata", JSON.stringify(metadata)) + + return this.request(path, { + method: "POST", + body + }) + } + + private async request( + path: string, + init: RequestInit + ): Promise { + const res = await fetch(this.baseUrl + path, { + ...init, + headers: { + ...(init.body instanceof FormData + ? {} + : { "Content-Type": "application/x-www-form-urlencoded" }) + } + }) + + if (!res.ok) { + const text = await res.text() + throw new Error(`API ${res.status}: ${text}`) + } + + return res.json() + } +} + +const RAG_Client = new RAGClient(process.env.DCUP_RAG ?? "http://127.0.0.1:8000") +export default RAG_Client diff --git a/DataSource/Aws/setAwsConnection.ts b/DataSource/Aws/setAwsConnection.ts index 0827fbb..1da5233 100644 --- a/DataSource/Aws/setAwsConnection.ts +++ b/DataSource/Aws/setAwsConnection.ts @@ -1,7 +1,7 @@ import { databaseDrizzle } from "@/db"; -import { connections } from "@/db/schemas/connections"; import { eq } from "drizzle-orm"; import { connectionConfig } from "../utils"; +import { connections } from "@/db/schema"; export const setAWSConnection = async (formData: FormData) => { @@ -16,13 +16,7 @@ export const setAWSConnection = async (formData: FormData) => { }) if (!config.success) { - const errors = config.error.errors - .map(err => { - const fieldPath = err.path.length > 0 ? err.path.join('.') : 'value' - return `"${fieldPath}": ${err.message}` - }) - .join('; ') - throw new Error(`Validation errors - ${errors}`) + throw new Error(`Validation errors - ${config.error.message}`) } await databaseDrizzle.update(connections).set({ diff --git a/DataSource/DirectUpload/SetNewConfigDirect/SetNewConfigDirect.tsx b/DataSource/DirectUpload/SetNewConfigDirect/SetNewConfigDirect.tsx index 642759b..83670ac 100644 --- a/DataSource/DirectUpload/SetNewConfigDirect/SetNewConfigDirect.tsx +++ b/DataSource/DirectUpload/SetNewConfigDirect/SetNewConfigDirect.tsx @@ -18,7 +18,7 @@ export const SetNewConfigDirect = () => { - + Upload Files Directly diff --git a/DataSource/DirectUpload/setDirectUploadConnection.ts b/DataSource/DirectUpload/setDirectUploadConnection.ts index aec1593..0dc082c 100644 --- a/DataSource/DirectUpload/setDirectUploadConnection.ts +++ b/DataSource/DirectUpload/setDirectUploadConnection.ts @@ -1,57 +1,10 @@ import { databaseDrizzle } from "@/db"; -import { connections, processedFiles } from "@/db/schemas/connections"; +import { connections, processedFiles } from "@/db/schema"; import { tryAndCatch } from "@/lib/try-catch"; import { qdrant_collection_name, qdrantClient } from "@/qdrant"; import { and, eq } from "drizzle-orm"; import { z } from "zod"; -const directUploadConfig = z.object({ - userId: z.string().min(5), - identifier: z.string().min(2), - metadata: z.string() - .transform((str, ctx): string => { - try { - if (str) { - JSON.parse(str) - return str - } - return "{}" - } catch (e) { - ctx.addIssue({ code: 'custom', message: 'Invalid JSON' }) - return z.NEVER - } - }), - pageLimit: z.string().nullable().transform((str, ctx): number | null => { - try { - if (str) return parseInt(str) - return null - } catch (error) { - ctx.addIssue({ code: 'invalid_date', message: "invalid page limit" }) - return z.NEVER - } - }), - fileLimit: z.string().nullable().transform((str, ctx): number | null => { - try { - if (str) return parseInt(str) - return null - } catch (error) { - ctx.addIssue({ code: 'invalid_date', message: "invalid page limit" }) - return z.NEVER - } - }), - files: z.array(z.any().refine((file) => { - return ( - file || - (file instanceof File && file.type === "application/pdf") - ); - }, - { - message: "Invalid File", - }) - ), - links: z.array(z.string().min(5)), - texts: z.array(z.string().min(5)), -}) const updateDirectUploadConfig = z.object({ userId: z.string().min(5), @@ -61,7 +14,7 @@ const updateDirectUploadConfig = z.object({ if (str) return parseInt(str) return null } catch (error) { - ctx.addIssue({ code: 'invalid_date', message: "invalid page limit" }) + ctx.addIssue({ code: 'custom', message: "invalid page limit" }) return z.NEVER } }), @@ -107,13 +60,8 @@ export const updateDirectUploadConnection = async (formData: FormData) => { }) if (!config.success) { - const errors = config.error.errors - .map(err => { - const fieldPath = err.path.length > 0 ? err.path.join('.') : 'value' - return `"${fieldPath}": ${err.message}` - }) - .join('; ') - throw new Error(`Validation errors - ${errors}`) + + throw new Error(`Validation errors - ${config.error.message}`) } const connectionChunksIds: { chunksIds: string[], name: string }[] = []; @@ -178,13 +126,7 @@ export const setDirectUploadConnection = async (formData: FormData) => { }) if (!config.success) { - const errors = config.error.errors - .map(err => { - const fieldPath = err.path.length > 0 ? err.path.join('.') : 'value' - return `"${fieldPath}": ${err.message}` - }) - .join('; ') - throw new Error(`Validation errors - ${errors}`) + throw new Error(`Validation errors - ${config.error.message}`) } const { files, links, userId, identifier, metadata, fileLimit, pageLimit, texts } = config.data; diff --git a/DataSource/Dropbox/setDropboxConnection.ts b/DataSource/Dropbox/setDropboxConnection.ts index ea195a6..8fb43ce 100644 --- a/DataSource/Dropbox/setDropboxConnection.ts +++ b/DataSource/Dropbox/setDropboxConnection.ts @@ -1,7 +1,7 @@ import { databaseDrizzle } from "@/db"; -import { connections } from "@/db/schemas/connections"; import { eq } from "drizzle-orm"; import { connectionConfig } from "../utils"; +import { connections } from "@/db/schema"; export const setDropboxConnection = async (formData: FormData) => { const config = connectionConfig.safeParse({ @@ -15,13 +15,7 @@ export const setDropboxConnection = async (formData: FormData) => { }) if (!config.success) { - const errors = config.error.errors - .map(err => { - const fieldPath = err.path.length > 0 ? err.path.join('.') : 'value' - return `"${fieldPath}": ${err.message}` - }) - .join('; ') - throw new Error(`Validation errors - ${errors}`) + throw new Error(`Validation errors - ${config.error.message}`) } await databaseDrizzle.update(connections).set({ diff --git a/DataSource/GoogleDrive/setGoogleDriveConnection.ts b/DataSource/GoogleDrive/setGoogleDriveConnection.ts index 77a6893..4a85258 100644 --- a/DataSource/GoogleDrive/setGoogleDriveConnection.ts +++ b/DataSource/GoogleDrive/setGoogleDriveConnection.ts @@ -1,7 +1,7 @@ import { databaseDrizzle } from "@/db"; -import { connections } from "@/db/schemas/connections"; import { eq } from "drizzle-orm"; import { connectionConfig } from "../utils"; +import { connections } from "@/db/schema"; export const setGoogleDriveConnection = async (formData: FormData) => { const config = connectionConfig.safeParse({ @@ -15,13 +15,7 @@ export const setGoogleDriveConnection = async (formData: FormData) => { }) if (!config.success) { - const errors = config.error.errors - .map(err => { - const fieldPath = err.path.length > 0 ? err.path.join('.') : 'value' - return `"${fieldPath}": ${err.message}` - }) - .join('; ') - throw new Error(`Validation errors - ${errors}`) + throw new Error(`Validation errors - ${config.error.message}`) } await databaseDrizzle.update(connections).set({ diff --git a/DataSource/utils.ts b/DataSource/utils.ts index 0b65c10..9a2de9a 100644 --- a/DataSource/utils.ts +++ b/DataSource/utils.ts @@ -26,7 +26,7 @@ export const connectionConfig = z.object({ if (str) return parseInt(str) return null } catch (error) { - ctx.addIssue({ code: 'invalid_date', message: "invalid page limit" }) + ctx.addIssue({ code: 'custom', message: "invalid page limit" }) return z.NEVER } }), @@ -35,7 +35,7 @@ export const connectionConfig = z.object({ if (str) return parseInt(str) return null } catch (error) { - ctx.addIssue({ code: 'invalid_date', message: "invalid page limit" }) + ctx.addIssue({ code: 'custom', message: "invalid page limit" }) return z.NEVER } }), diff --git a/actions/apiKeys.ts b/actions/apiKeys.ts index 618b4da..753ed39 100644 --- a/actions/apiKeys.ts +++ b/actions/apiKeys.ts @@ -1,13 +1,13 @@ "use server"; -import { authOptions } from "@/auth"; import { databaseDrizzle } from "@/db"; import { apiKeys } from "@/db/schemas/users"; import { apiKeyGenerator, hashApiKey } from "@/lib/api_key"; import { fromErrorToFormState, toFormState } from "@/lib/zodErrorHandle"; +import { auth } from "@/lib/auth"; import { eq } from "drizzle-orm"; -import { getServerSession } from "next-auth"; import { revalidatePath } from "next/cache"; import { z } from "zod"; +import { headers } from "next/headers"; const apiScheme = z.object({ name: z.string().min(2), @@ -17,7 +17,9 @@ type FormState = { message: string; }; export async function generateApiKey(_: FormState, formData: FormData) { - const session = await getServerSession(authOptions); + const session = await auth.api.getSession({ + headers: await headers(), + }) try { if (!session?.user?.id) throw new Error("forbidden"); @@ -43,7 +45,10 @@ export async function generateApiKey(_: FormState, formData: FormData) { } export async function deleteApiKey(_: FormState, formData: FormData) { - const session = await getServerSession(authOptions); + const session = await auth.api.getSession({ + headers: await headers(), + }) + try { if (!session?.user?.id) throw new Error("forbidden"); // if (!hasAuthority(plan.toString(), new Date(session.user.createdAt!))) throw new Error("Your free plan has expired. Please subscribe to continue using the app.") diff --git a/actions/aws/index.ts b/actions/aws/index.ts deleted file mode 100644 index fa0c330..0000000 --- a/actions/aws/index.ts +++ /dev/null @@ -1,142 +0,0 @@ -'use server' -import { fromErrorToFormState, toFormState } from "@/lib/zodErrorHandle"; -import { GetObjectCommand, ListBucketsCommand, ListObjectsV2Command, S3Client } from "@aws-sdk/client-s3"; -import { revalidatePath } from "next/cache"; -import { tryAndCatch } from "@/lib/try-catch"; -import { redirect } from 'next/navigation'; -import { databaseDrizzle } from "@/db"; -import { connections } from "@/db/schemas/connections"; -import { shortId } from "@/lib/utils"; -import { getServerSession } from "next-auth"; -import { authOptions } from "@/auth"; -import { z } from 'zod' - -const awsConnectionSchema = z.object({ - accessKeyId: z.string().min(16, 'Invalid Access Key ID'), - secretAccessKey: z.string().min(1, 'Secret Access Key is required'), - region: z.string().min(1, 'Region is required'), - endpoint: z.string().optional().nullable(), -}); - -type FormState = { - message: string; -}; - -export async function authorizeAWS(_: FormState, formData: FormData) { - try { - const session = await getServerSession(authOptions) - if (!session?.user?.id) throw new Error("forbidden"); - - const validated = awsConnectionSchema.safeParse({ - accessKeyId: formData.get('accessKeyId'), - secretAccessKey: formData.get('secretKey'), - region: formData.get('region'), - endpoint: formData.get('endpoint'), - }); - if (!validated.success) { - throw new Error(validated.error.errors[0].message) - } - - const s3Config = { - credentials: { - accessKeyId: validated.data.accessKeyId, - secretAccessKey: validated.data.secretAccessKey, - }, - region: validated.data.region, - ...(validated.data.endpoint && { endpoint: validated.data.endpoint }), - }; - - const s3Client = new S3Client(s3Config); - const { Buckets } = await s3Client.send(new ListBucketsCommand({})); - const { error: objectError } = await tryAndCatch(s3Client.send(new GetObjectCommand({ - Bucket: Buckets![0].Name!, - Key: "__test_permissions_file__", - }))) - if (objectError) { - if (objectError.name !== "NoSuchKey") throw objectError - } - - await databaseDrizzle - .insert(connections) - .values({ - userId: session.user.id!, - identifier: shortId(15), - service: 'AWS', - connectionMetadata: { - folderId: "" - }, - credentials: { - accessKeyId: validated.data.accessKeyId, - secretAccessKey: validated.data.secretAccessKey, - region: validated.data.region, - endpoint: validated.data.endpoint, - }, - }) - .onConflictDoUpdate({ - target: [connections.identifier], - set: { identifier: shortId(16) }, - }); - } catch (e) { - revalidatePath("/authorized/callback/aws"); - return fromErrorToFormState(e); - } - redirect("/connections") -} - -export async function loadBuckets(_: FormState, formData: FormData) { - try { - - const { accessKeyId, secretAccessKey, region, endpoint } = - awsConnectionSchema.parse( - JSON.parse(formData.get("credentials")?.toString() || "{}") - ) - - const client = new S3Client({ - credentials: { - accessKeyId: accessKeyId, - secretAccessKey: secretAccessKey, - }, - region: region, - endpoint: endpoint || undefined, - }); - const { Buckets } = await client.send(new ListBucketsCommand({})) - - revalidatePath("/connections"); - return toFormState("SUCCESS", JSON.stringify(Buckets?.flatMap(b => b.Name!) || '[]') || "[]"); - } catch (e) { - return fromErrorToFormState(e); - } -} - -export async function loadFolders(_: FormState, formData: FormData) { - try { - const { accessKeyId, secretAccessKey, region, endpoint } = - awsConnectionSchema.parse( - JSON.parse(formData.get("credentials")?.toString() || "{}") - ); - const bucket = formData.get("bucket"); - const prefix = formData.get("prefix")?.toString() || ''; - - if (!bucket) throw new Error("bucket name required"); - - const client = new S3Client({ - credentials: { - accessKeyId: accessKeyId, - secretAccessKey: secretAccessKey, - }, - region: region, - endpoint: endpoint || undefined, - }); - - const { CommonPrefixes } = await client.send(new ListObjectsV2Command({ - Bucket: bucket.toString(), - Delimiter: '/', - Prefix: prefix // Add prefix to the command - })); - - const folders = CommonPrefixes?.flatMap(p => p.Prefix?.replace(prefix, '')) || []; - return toFormState("SUCCESS", JSON.stringify(folders || '[]') || '[]'); - } catch (e) { - return fromErrorToFormState(e); - } -} diff --git a/actions/connctions/delete/index.ts b/actions/connctions/delete/index.ts deleted file mode 100644 index 8d797ef..0000000 --- a/actions/connctions/delete/index.ts +++ /dev/null @@ -1,55 +0,0 @@ -"use server" -import { authOptions } from "@/auth"; -import { databaseDrizzle } from "@/db"; -import { connections, processedFiles } from "@/db/schemas/connections"; -import { tryAndCatch } from "@/lib/try-catch"; -import { fromErrorToFormState, toFormState } from "@/lib/zodErrorHandle"; -import { qdrant_collection_name, qdrantClient } from "@/qdrant"; -import { eq } from "drizzle-orm"; -import { getServerSession } from "next-auth"; -import { revalidatePath } from "next/cache"; -import { z } from "zod"; - -const deleteConnectionSchema = z.object({ - id: z.string().min(2), -}); - -type FormState = { - message: string; -}; - -export async function deleteConnectionConfig(_: FormState, formData: FormData) { - const session = await getServerSession(authOptions); - try { - if (!session?.user?.id) throw new Error("forbidden"); - const { id } = deleteConnectionSchema.parse({ - id: formData.get("connectionId"), - }) - - const connectionChunksIds = await databaseDrizzle - .select({ chunksIds: processedFiles.chunksIds, name: processedFiles.name }) - .from(processedFiles) - .where(eq(processedFiles.connectionId, id)) - - for (const { chunksIds, name } of connectionChunksIds) { - await tryAndCatch(qdrantClient.delete(qdrant_collection_name, { - points: chunksIds, - filter: { - must: [ - { key: "_document_id", "match": { value: name } }, - { key: "_userId", match: { value: session.user.id } }] - } - })) - } - - await databaseDrizzle - .delete(connections) - .where(eq(connections.id, id)) - - revalidatePath("/connections"); - return toFormState("SUCCESS", "Connection Deleted Successfully"); - - } catch (e) { - return fromErrorToFormState(e); - } -} diff --git a/actions/connctions/new/index.ts b/actions/connctions/new/index.ts deleted file mode 100644 index 529ab35..0000000 --- a/actions/connctions/new/index.ts +++ /dev/null @@ -1,64 +0,0 @@ -"use server" -import { authOptions } from "@/auth"; -import { databaseDrizzle } from "@/db"; -import { authDropbox } from "@/fileProcessors/connectors/dropbox"; -import { authGoogleDrive } from "@/fileProcessors/connectors/googleDrive"; -import { Plans } from "@/lib/Plans"; -import { fromErrorToFormState, toFormState } from "@/lib/zodErrorHandle"; -import { getServerSession } from "next-auth"; -import { revalidatePath } from "next/cache"; - -type FormState = { - message: string; -}; - -export async function newConnection(_: FormState, formData: FormData) { - const connection = formData.get("connection") - let redirectUrl: string; - - try { - const session = await getServerSession(authOptions) - if (!session?.user?.id) throw new Error("forbidden"); - const user = await databaseDrizzle.query.users.findFirst({ - where: (u, ops) => ops.eq(u.id, session.user.id!), - columns: { - plan: true, - }, - with: { - connections: { - columns: { - id: true, - } - } - } - }) - if (!user) throw new Error("no such account") - const plan = Plans[user.plan] - const used = user.connections.length; - if (used >= plan.connections) { - throw new Error( - `You’ve reached your connection limit for the ${user.plan.toLowerCase()} plan (` + - `${used}/${plan.connections}). ` + - `To add more connections, please upgrade your subscription.` - ); - } - - switch (connection) { - case "google-drive": - redirectUrl = authGoogleDrive() - break; - case "dropbox": - redirectUrl = await authDropbox() - break; - case "aws": - redirectUrl = "/authorized/callback/aws" - break; - default: - throw new Error("Unknown provider"); - } - revalidatePath("/connections"); - return toFormState("SUCCESS", redirectUrl); - } catch (e) { - return fromErrorToFormState(e); - } -} diff --git a/actions/files/delete/index.ts b/actions/files/delete/index.ts new file mode 100644 index 0000000..6730dfa --- /dev/null +++ b/actions/files/delete/index.ts @@ -0,0 +1,54 @@ +"use server" +import { databaseDrizzle } from "@/db"; +import { tryAndCatch } from "@/lib/try-catch"; +import { fromErrorToFormState, toFormState } from "@/lib/zodErrorHandle"; +import { qdrant_collection_name, qdrantClient } from "@/qdrant"; +import { auth } from "@/lib/auth"; +import { and, eq } from "drizzle-orm"; +import { revalidatePath } from "next/cache"; +import { z } from "zod"; +import { headers } from "next/headers"; +import { files } from "@/db/schema"; + +const deleteFilesSchema = z.object({ + id: z.string().min(2), +}); + +type FormState = { + message: string; +}; + +export async function deleteFilesAction(_: FormState, formData: FormData) { + const session = await auth.api.getSession({ + headers: await headers(), + }) + try { + if (!session?.user?.id) throw new Error("forbidden"); + const { id } = deleteFilesSchema.parse({ + id: formData.get("file_id"), + }) + + const { error, data } = await tryAndCatch(qdrantClient.delete(qdrant_collection_name, { + filter: { + must: [ + { key: "_file_id", match: { value: id } }, + { key: "_user_id", match: { value: session.user.id } }] + }, + wait: true + })) + if (error) { + throw new Error(error.message) + } + if (data.status === 'completed') { + await databaseDrizzle + .delete(files) + .where(and(eq(files.id, id), eq(files.userId, session.user.id))) + } + + revalidatePath("/connections"); + return toFormState("SUCCESS", "Connection Deleted Successfully"); + + } catch (e) { + return fromErrorToFormState(e); + } +} diff --git a/actions/files/new/index.ts b/actions/files/new/index.ts new file mode 100644 index 0000000..f2700ab --- /dev/null +++ b/actions/files/new/index.ts @@ -0,0 +1,56 @@ +"use server" +import { databaseDrizzle } from "@/db"; +import { Plans } from "@/lib/Plans"; +import { fromErrorToFormState, toFormState } from "@/lib/zodErrorHandle"; +import { auth } from "@/lib/auth"; +import { revalidatePath } from "next/cache"; +import { headers } from "next/headers"; +import RAG_Client from "@/Clients/RAG_Client"; + +type FormState = { + message: string; +}; + +export async function newFilesAction(_: FormState, formData: FormData) { + const files = formData.getAll("files") as File[] + const links = formData.getAll("links") as string[] + const metadata = formData.get("metadata") as string + + try { + const session = await auth.api.getSession({ + headers: await headers(), + }) + if (!session?.user?.id) throw new Error("forbidden"); + const user = await databaseDrizzle.query.users.findFirst({ + where: (u, ops) => ops.eq(u.id, session.user.id!), + columns: { + plan: true, + }, + with: { + files: true, + } + }) + if (!user) throw new Error("no such account") + const plan = Plans[user.plan] + const used = user.files.length; + if (used >= plan.connections) { + throw new Error( + `You’ve reached your file upload limit for the ${user.plan.toLowerCase()} plan (` + + `${used}/${plan.connections}). ` + + `To add more connections, please upgrade your subscription.` + ); + } + const metaJson = JSON.parse(metadata || "{}") + metaJson["_user_id"] = session.user.id + const meta = JSON.stringify(metaJson) + + const storeFiles = files.map(file => RAG_Client.store.file.all(file, meta)) + const storeLinks = links.map(link => RAG_Client.store.url.all(link, meta)) + await Promise.all([...storeFiles, ...storeLinks]) + + revalidatePath("/documents"); + return toFormState("SUCCESS", "Upload successful"); + } catch (e) { + return fromErrorToFormState(e); + } +} diff --git a/actions/connctions/set/index.ts b/actions/files/set/index.ts similarity index 81% rename from actions/connctions/set/index.ts rename to actions/files/set/index.ts index 2a46990..e482e8a 100644 --- a/actions/connctions/set/index.ts +++ b/actions/files/set/index.ts @@ -1,19 +1,21 @@ "use server" -import { authOptions } from "@/auth"; import { databaseDrizzle } from "@/db"; -import { connections } from "@/db/schemas/connections"; import { setConnectionToProcess } from "@/fileProcessors/connectors"; import { fromErrorToFormState, toFormState } from "@/lib/zodErrorHandle"; import { addToProcessFilesQueue } from "@/workers/queues/jobs/processFiles.job"; -import { getServerSession } from "next-auth"; +import { auth } from "@/lib/auth"; import { revalidatePath } from "next/cache"; +import { headers } from "next/headers"; +import { connections } from "@/db/schema"; type FormState = { message: string; }; export async function setConnectionConfig(_: FormState, formData: FormData) { - const session = await getServerSession(authOptions); + const session = await auth.api.getSession({ + headers: await headers(), + }) try { if (!session?.user?.id) throw new Error("forbidden"); diff --git a/actions/connctions/stop/index.ts b/actions/files/stop/index.ts similarity index 86% rename from actions/connctions/stop/index.ts rename to actions/files/stop/index.ts index 331e4c6..aa9a898 100644 --- a/actions/connctions/stop/index.ts +++ b/actions/files/stop/index.ts @@ -1,16 +1,18 @@ "use server" -import { authOptions } from "@/auth"; import { databaseDrizzle } from "@/db"; import { fromErrorToFormState, toFormState } from "@/lib/zodErrorHandle"; import { redisConnection } from "@/workers/redis"; -import { getServerSession } from "next-auth"; +import { auth } from "@/lib/auth"; +import { headers } from "next/headers"; type FormState = { message: string; }; export async function stopProcessing(_: FormState, formData: FormData) { - const session = await getServerSession(authOptions); + const session = await auth.api.getSession({ + headers: await headers(), +}) try { if (!session?.user?.id) throw new Error("forbidden"); const connectionId = formData.get("connectionId"); diff --git a/actions/connctions/sync/index.ts b/actions/files/sync/index.ts similarity index 92% rename from actions/connctions/sync/index.ts rename to actions/files/sync/index.ts index 53a09e6..5c18171 100644 --- a/actions/connctions/sync/index.ts +++ b/actions/files/sync/index.ts @@ -1,13 +1,14 @@ "use server" -import { authOptions } from "@/auth"; import { databaseDrizzle } from "@/db"; -import { connections } from "@/db/schemas/connections"; import { Plans } from "@/lib/Plans"; import { fromErrorToFormState, toFormState } from "@/lib/zodErrorHandle"; import { addToProcessFilesQueue } from "@/workers/queues/jobs/processFiles.job"; import { eq } from "drizzle-orm"; -import { getServerSession } from "next-auth"; import { revalidatePath } from "next/cache"; +import { auth } from "@/lib/auth"; +import { headers } from "next/headers"; +import { connections } from "@/db/schema"; + type FormState = { message: string; @@ -26,7 +27,9 @@ type Conn = { export const syncConnectionConfig = async (_: FormState, formData: FormData) => { - const session = await getServerSession(authOptions) + const session = await auth.api.getSession({ + headers: await headers(), + }) const connectionId = formData.get("connectionId")?.toString(); try { if (!session?.user?.id || !connectionId) throw new Error("forbidden"); diff --git a/app/(protected)/connections/new/page.tsx b/app/(protected)/connections/new/page.tsx index 28dc284..2250cda 100644 --- a/app/(protected)/connections/new/page.tsx +++ b/app/(protected)/connections/new/page.tsx @@ -1,18 +1,20 @@ import Link from 'next/link' -import { authOptions } from '@/auth' import { Connectors } from '@/components/Connectors/Connectors'; import { Button } from '@/components/ui/button'; -import { getServerSession } from 'next-auth' import { redirect } from 'next/navigation'; +import { auth } from '@/lib/auth'; +import { headers } from 'next/headers'; export default async function page() { - const session = await getServerSession(authOptions) + const session = await auth.api.getSession({ + headers: await headers(), + }) if (!session?.user.id) return redirect("/login") return (
-

+

Connected Services

diff --git a/app/(protected)/connections/page.tsx b/app/(protected)/connections/page.tsx index 24390ef..435a3e0 100644 --- a/app/(protected)/connections/page.tsx +++ b/app/(protected)/connections/page.tsx @@ -1,33 +1,24 @@ import Link from "next/link" import dynamic from 'next/dynamic' -import { authOptions } from "@/auth"; import { Button } from "@/components/ui/button"; import { databaseDrizzle } from "@/db"; -import { getServerSession } from "next-auth"; import { redirect } from 'next/navigation'; -import { ConnectionTable } from "@/db/schemas/connections"; -import { Table, TableBody, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { getConnectionToken } from "@/fileProcessors/connectors"; -import { FiDatabase } from "react-icons/fi"; import { SetNewConfigDirect } from "@/DataSource/DirectUpload/SetNewConfigDirect/SetNewConfigDirect"; import { tryAndCatch } from "@/lib/try-catch"; +import { auth } from "@/lib/auth"; +import { headers } from "next/headers"; -const Connections = dynamic(() => import('@/components/Connections/Connections')) -export interface ConnectionQuery extends ConnectionTable { - files: { - totalPages: number, - name: string, - }[] -} export type ConnectionToken = Map; export default async function ConnectionsPage() { - const session = await getServerSession(authOptions) + const session = await auth.api.getSession({ + headers: await headers(), + }) if (!session?.user.id) return redirect("/login") - - const connections: ConnectionQuery[] = await databaseDrizzle.query.connections.findMany({ - where: (c, ops) => ops.eq(c.userId, session.user.id!), + const connections = await databaseDrizzle.query.users.findMany({ + where: (c, ops) => ops.eq(c.id, session.user.id!), with: { files: { columns: { @@ -38,11 +29,17 @@ export default async function ConnectionsPage() { } }) + const tokens: ConnectionToken = new Map() + for (const conn of connections) { + const { data } = await tryAndCatch(getConnectionToken(conn)) + tokens.set(conn.id, data || null) + } + return (

-

+

Connected Services

@@ -58,53 +55,7 @@ export default async function ConnectionsPage() {

- {connections.length === 0 ? () - : ()} -
- ); -} - -async function CurrentConnections({ connections }: { connections: ConnectionQuery[] }) { - const tokens: ConnectionToken = new Map() - for (const conn of connections) { - const { data } = await tryAndCatch(getConnectionToken(conn)) - tokens.set(conn.id, data || null) - } - - return ( -
-

Active Connections

-
- - - - Source - Directory - Documents - Pages - Date Added - Last Synced - - - - - -
-
-
- ); -} - -function EmptyState() { - return ( -
-
- -
-

No Connected Sources

-

- Connect your first data source to start syncing documents and pages with your application. We support Google Drive, Notion, AWS, and more. -

+
); } diff --git a/app/(protected)/documents/page.tsx b/app/(protected)/documents/page.tsx new file mode 100644 index 0000000..fab40af --- /dev/null +++ b/app/(protected)/documents/page.tsx @@ -0,0 +1,46 @@ +import { databaseDrizzle } from "@/db"; +import { redirect } from 'next/navigation'; +import { auth } from "@/lib/auth"; +import { headers } from "next/headers"; +import { UploadFilesDialog } from "@/components/UploadFiles/UploadFies"; +import { FileTable } from "@/components/FileTable/FileTable"; + + +export type ConnectionToken = Map; + +export default async function DocumentsPage() { + const session = await auth.api.getSession({ + headers: await headers(), + }) + if (!session?.user.id) return redirect("/login") + const connections = await databaseDrizzle.query.users.findMany({ + where: (c, ops) => ops.eq(c.id, session.user.id!), + with: { + files: { + columns: { + totalPages: true, + name: true, + } + } + } + }) + + return ( +
+
+
+

+ Connected Services +

+

+ Manage your data sources and keep your application in sync. +

+
+
+ +
+
+ +
+ ); +} diff --git a/app/(protected)/integration/page.tsx b/app/(protected)/integration/page.tsx index 5a48689..906934d 100644 --- a/app/(protected)/integration/page.tsx +++ b/app/(protected)/integration/page.tsx @@ -1,15 +1,16 @@ -import React from 'react' -import { authOptions } from '@/auth'; import { GenerateKeyForm } from '@/components/GeneratekeyForm/GeneratekeyForm'; import { KeysList } from '@/components/keysList/KeysList'; import { CardHeader, Card, CardTitle, CardDescription, CardContent } from '@/components/ui/card'; -import { getServerSession } from 'next-auth'; import { redirect } from 'next/navigation'; +import { auth } from '@/lib/auth'; +import { headers } from 'next/headers'; export default async function IntegrationPage() { - const session = await getServerSession(authOptions); - if (!session?.user.id) return redirect("/login") + const session = await auth.api.getSession({ + headers: await headers(), + }) + if (!session?.user.id) return redirect("/login") return (
diff --git a/app/(protected)/page.tsx b/app/(protected)/page.tsx index cb1e808..35ea08f 100644 --- a/app/(protected)/page.tsx +++ b/app/(protected)/page.tsx @@ -1,10 +1,7 @@ import PipelineFlow from "@/components/PipelineFlow/PipelineFlow"; import { formatDistanceToNow } from 'date-fns' import { databaseDrizzle } from "@/db"; -import { getServerSession } from "next-auth"; -import { authOptions } from "@/auth"; import { redirect } from 'next/navigation'; -import { ConnectionTable, ProcessedFilesTable } from "@/db/schemas/connections"; import { getServiceIcon } from "@/lib/helepers"; import { FaFilePdf } from "react-icons/fa"; import { TooltipContent, TooltipTrigger, Tooltip } from "@/components/ui/tooltip"; @@ -17,13 +14,15 @@ import { TableRow, } from "@/components/ui/table" import { SubscriptionCard } from "@/components/SubscriptionCard/SubscriptionCard"; - -interface FileConnectionQuery extends ConnectionTable { - files: ProcessedFilesTable[] -} +import { auth } from "@/lib/auth"; +import { headers } from "next/headers"; +import { ProcessedFilesTable } from "@/db/schema"; export default async function page() { - const session = await getServerSession(authOptions) + const session = await auth.api.getSession({ + headers: await headers(), + }) + if (!session?.user.id) return redirect("/login") const user = await databaseDrizzle.query.users.findFirst({ @@ -33,60 +32,52 @@ export default async function page() { plan: true, }, with: { - connections: { - with: { - files: true, - } - } + files: true } }) if (!user) return redirect("/api/auth/signout") - const totalPages = user.connections - .flatMap(conn => conn.files || []) + const totalPages = user.files .reduce((sum, file) => sum + (file.totalPages || 0), 0); - return ( -
-

- RAG Pipeline Dashboard -

- - {/* Visualization Section */} - - - {/* Data Section */} -
- {/* Files Table */} -
-
- -
-
+ return (
+

+ RAG Pipeline Dashboard +

+ + {/* Visualization Section */} + {/* */} - {/* API Usage */} -
- + {/* Data Section */} +
+ {/* Files Table */} +
+
+ *
+ + {/* API Usage */} +
+ {/* */} +
+
) } -function FilesTable({ connections }: { connections: FileConnectionQuery[] }) { - const allFiles = connections.flatMap(conn => - conn.files.map(file => ({ ...file, connection: conn }))) +function FilesTable({ files }: { files: ProcessedFilesTable[] }) { - if (allFiles.length === 0) return ( + if (files.length === 0) return (
📁
@@ -109,7 +100,7 @@ function FilesTable({ connections }: { connections: FileConnectionQuery[] }) { - {allFiles.map((file) => ( + {files.map((file) => (
- {getServiceIcon(file.connection.service)} + {/* {getServiceIcon(file.connection.service)} */}
- {file.connection.service.toLowerCase().replace('_', ' ')} + {/* {file.connection.service.toLowerCase().replace('_', ' ')} */} - {file.connection.identifier} + {/* {file.connection.identifier} */}
@@ -152,13 +143,13 @@ function FilesTable({ connections }: { connections: FileConnectionQuery[] }) { - {formatDistanceToNow(new Date(file.connection.lastSynced || ""), { + {formatDistanceToNow(new Date(file.createdAt || ""), { addSuffix: true })} - {new Date(file.connection.lastSynced || "").toLocaleString()} + {new Date(file.createdAt || "").toLocaleString()} diff --git a/app/api/auth/[...all]/route.ts b/app/api/auth/[...all]/route.ts new file mode 100644 index 0000000..e9ace87 --- /dev/null +++ b/app/api/auth/[...all]/route.ts @@ -0,0 +1,3 @@ +import { auth } from "@/lib/auth"; +import { toNextJsHandler } from "better-auth/next-js"; +export const { POST, GET } = toNextJsHandler(auth); diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts deleted file mode 100644 index 8796a7a..0000000 --- a/app/api/auth/[...nextauth]/route.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { authOptions } from "@/auth"; -import NextAuth from "next-auth"; -const handler = NextAuth(authOptions); -export { handler as GET, handler as POST }; diff --git a/app/api/connections/[id]/files/route.ts b/app/api/connections/[id]/files/route.ts index 604b9d1..796ddc8 100644 --- a/app/api/connections/[id]/files/route.ts +++ b/app/api/connections/[id]/files/route.ts @@ -1,5 +1,5 @@ import { databaseDrizzle } from "@/db" -import { processedFiles } from "@/db/schemas/connections" +import { processedFiles } from "@/db/schema" import { checkAuth } from "@/lib/api_key" import { tryAndCatch } from "@/lib/try-catch" import { qdrant_collection_name, qdrantClient } from "@/qdrant" diff --git a/app/api/connections/[id]/route.ts b/app/api/connections/[id]/route.ts index a3b5ad3..3ac885b 100644 --- a/app/api/connections/[id]/route.ts +++ b/app/api/connections/[id]/route.ts @@ -4,11 +4,11 @@ import { NextRequest, NextResponse } from "next/server"; import { APIError } from "@/lib/APIError"; import { databaseDrizzle } from "@/db"; import { eq } from "drizzle-orm"; -import { connections } from "@/db/schemas/connections"; import { qdrant_collection_name, qdrantClient } from "@/qdrant"; import { setConnectionToProcess } from "@/fileProcessors/connectors"; import { connectionProcessFiles, directProcessFiles } from "@/fileProcessors"; import { addToProcessFilesQueue } from "@/workers/queues/jobs/processFiles.job"; +import { connections } from "@/db/schema"; type Params = { params: Promise<{ diff --git a/app/api/connections/dropbox/callback/route.ts b/app/api/connections/dropbox/callback/route.ts index c14051f..0f6cdc6 100644 --- a/app/api/connections/dropbox/callback/route.ts +++ b/app/api/connections/dropbox/callback/route.ts @@ -1,11 +1,11 @@ import { NextResponse } from 'next/server'; import { Dropbox, DropboxAuth } from 'dropbox'; -import { getServerSession } from 'next-auth'; -import { authOptions } from '@/auth'; import { tryAndCatch } from '@/lib/try-catch'; import { databaseDrizzle } from '@/db'; -import { connections } from '@/db/schemas/connections'; import { shortId } from '@/lib/utils'; +import { auth } from '@/lib/auth'; +import { headers } from 'next/headers'; +import { connections } from '@/db/schema'; type DropboxResponse = { access_token: string, @@ -20,7 +20,9 @@ type DropboxResponse = { export async function GET(request: Request) { try { - const session = await getServerSession(authOptions); + const session = await auth.api.getSession({ + headers: await headers(), + }) if (!session?.user.id) { return NextResponse.json( { code: 'Unauthorized', message: "Unauthorized Request" }, diff --git a/app/api/connections/google-drive/callback/route.ts b/app/api/connections/google-drive/callback/route.ts index 4390fb0..563ae69 100644 --- a/app/api/connections/google-drive/callback/route.ts +++ b/app/api/connections/google-drive/callback/route.ts @@ -1,12 +1,11 @@ import { NextResponse } from 'next/server'; import { auth, oauth2 } from '@googleapis/oauth2'; import { databaseDrizzle } from '@/db'; -import { connections } from '@/db/schemas/connections'; -import { getServerSession } from 'next-auth'; -import { authOptions } from '@/auth'; import { shortId } from '@/lib/utils'; import { tryAndCatch } from '@/lib/try-catch'; - +import { auth as authReq } from "@/lib/auth"; +import { headers } from "next/headers"; +import { connections } from '@/db/schema'; const oauth2Client = new auth.OAuth2( process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID, process.env.GOOGLE_CLIENT_SECRET, @@ -15,15 +14,9 @@ const oauth2Client = new auth.OAuth2( export async function GET(request: Request) { try { - const sessRes = await tryAndCatch(getServerSession(authOptions)); - if (sessRes.error) { - return NextResponse.json( - { code: 'Unauthorized', message: sessRes.error.message }, - { status: 500 }, - ); - } - - const session = sessRes.data; + const session = await authReq.api.getSession({ + headers: await headers(), + }) if (!session?.user.id) { return new Response('Unauthorized', { status: 401 }); } diff --git a/app/api/retrievals/route.ts b/app/api/retrievals/route.ts index 2bdca6e..6740005 100644 --- a/app/api/retrievals/route.ts +++ b/app/api/retrievals/route.ts @@ -1,16 +1,15 @@ -import { authOptions } from '@/auth'; import { databaseDrizzle } from '@/db'; -import { apiKeys, users } from '@/db/schemas/users'; import { hashApiKey } from '@/lib/api_key'; import { Plans } from '@/lib/Plans'; import { expandQuery, generateHypotheticalAnswer, vectorizeText } from '@/openAi'; import { qdrant_collection_name, qdrantClient } from '@/qdrant'; import { RetrievalFilter } from '@/validations/retrievalsFilteringSchema' import { and, eq, sql } from 'drizzle-orm'; -import { getServerSession } from 'next-auth'; import { NextRequest, NextResponse } from 'next/server' import { z } from 'zod' - +import { auth } from "@/lib/auth"; +import { headers } from "next/headers"; +import { apiKeys, users } from '@/db/schema'; const UserQuery = z.object({ query: z.string().min(2), @@ -34,9 +33,9 @@ export async function POST(request: NextRequest) { ); } - const auth = request.headers.get("Authorization"); - const bearer = auth?.split("Bearer ")[1] - if (!auth || !bearer) { + const apiKey = request.headers.get("Authorization"); + const bearer = apiKey?.split("Bearer ")[1] + if (!apiKey || !bearer) { return NextResponse.json( { code: "unauthorized", @@ -49,10 +48,12 @@ export async function POST(request: NextRequest) { let userId: string | undefined; if (bearer === "Dcup_Client") { - const session = await getServerSession(authOptions) + const session = await auth.api.getSession({ + headers: await headers(), + }) userId = session?.user.id } else { - const keyHashed = hashApiKey(auth.split("Bearer ")[1]); + const keyHashed = hashApiKey(apiKey.split("Bearer ")[1]); const key = await databaseDrizzle .select({ userId: apiKeys.userId }) .from(apiKeys) @@ -75,6 +76,7 @@ export async function POST(request: NextRequest) { columns: { plan: true, apiCalls: true, + id:true, } }) @@ -88,14 +90,14 @@ export async function POST(request: NextRequest) { const retrievalLimit = Plans[user.plan].retrievals; const updated = await databaseDrizzle .update(users) - .set({ apiCalls: sql`${users.apiCalls} + 1` }) + .set({ apiCalls: sql`${user.apiCalls} + 1` }) .where( and( - eq(users.id, userId), - sql`${users.apiCalls} < ${retrievalLimit}` + eq(user.id, userId), + sql`${user.apiCalls} < ${retrievalLimit}` ) ) - .returning({ apiCalls: users.apiCalls }) + .returning({ apiCalls: user.apiCalls }) .then(rows => rows[0]); if (!updated) { diff --git a/app/api/webhooks/paddle/route.ts b/app/api/webhooks/paddle/route.ts index c381ac5..b22b241 100644 --- a/app/api/webhooks/paddle/route.ts +++ b/app/api/webhooks/paddle/route.ts @@ -1,5 +1,5 @@ import { databaseDrizzle } from '@/db'; -import { users } from '@/db/schemas/users'; +import { users } from '@/db/schema'; import { Plans } from '@/lib/Plans'; import { Paddle, EventName, EventEntity } from '@paddle/paddle-node-sdk' import { eq } from 'drizzle-orm'; diff --git a/app/apple-icon.png b/app/apple-icon.png index d680f87..f02952f 100644 Binary files a/app/apple-icon.png and b/app/apple-icon.png differ diff --git a/app/authorized/layout.tsx b/app/authorized/layout.tsx index f327362..8320c97 100644 --- a/app/authorized/layout.tsx +++ b/app/authorized/layout.tsx @@ -1,11 +1,14 @@ -import { authOptions } from "@/auth"; -import { getServerSession } from "next-auth"; +import { auth } from "@/lib/auth"; +import { headers } from "next/headers"; import { notFound } from "next/navigation"; import { ReactNode } from "react"; export default async function Layout({ children }: { children: ReactNode }) { - const session = await getServerSession(authOptions) + const session = await auth.api.getSession({ + headers: await headers(), + }) + if (!session?.user.id) return notFound() return (
diff --git a/app/favicon.ico b/app/favicon.ico index 38b626a..6f6ece8 100644 Binary files a/app/favicon.ico and b/app/favicon.ico differ diff --git a/app/global.css b/app/global.css index a356dc9..fbbd6b3 100644 --- a/app/global.css +++ b/app/global.css @@ -1,112 +1,119 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; -@layer base { - :root { - --background: 0 0% 100%; - --foreground: 0 0% 3.9%; - --card: 0 0% 100%; - --card-foreground: 0 0% 3.9%; - --popover: 0 0% 100%; - --popover-foreground: 0 0% 3.9%; - --primary: 0 0% 9%; - --primary-foreground: 0 0% 98%; - --secondary: 0 0% 96.1%; - --secondary-foreground: 0 0% 9%; - --muted: 0 0% 96.1%; - --muted-foreground: 0 0% 45.1%; - --accent: 0 0% 96.1%; - --accent-foreground: 0 0% 9%; - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 0 0% 98%; - --border: 0 0% 89.8%; - --input: 0 0% 89.8%; - --ring: 0 0% 3.9%; - --chart-1: 12 76% 61%; - --chart-2: 173 58% 39%; - --chart-3: 197 37% 24%; - --chart-4: 43 74% 66%; - --chart-5: 27 87% 67%; - --radius: 0.5rem - ; - --sidebar-background: 0 0% 98%; - --sidebar-foreground: 240 5.3% 26.1%; - --sidebar-primary: 240 5.9% 10%; - --sidebar-primary-foreground: 0 0% 98%; - --sidebar-accent: 240 4.8% 95.9%; - --sidebar-accent-foreground: 240 5.9% 10%; - --sidebar-border: 220 13% 91%; - --sidebar-ring: 217.2 91.2% 59.8%} - .dark { - --background: 0 0% 3.9%; - --foreground: 0 0% 98%; - --card: 0 0% 3.9%; - --card-foreground: 0 0% 98%; - --popover: 0 0% 3.9%; - --popover-foreground: 0 0% 98%; - --primary: 0 0% 98%; - --primary-foreground: 0 0% 9%; - --secondary: 0 0% 14.9%; - --secondary-foreground: 0 0% 98%; - --muted: 0 0% 14.9%; - --muted-foreground: 0 0% 63.9%; - --accent: 0 0% 14.9%; - --accent-foreground: 0 0% 98%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 0 0% 98%; - --border: 0 0% 14.9%; - --input: 0 0% 14.9%; - --ring: 0 0% 83.1%; - --chart-1: 220 70% 50%; - --chart-2: 160 60% 45%; - --chart-3: 30 80% 55%; - --chart-4: 280 65% 60%; - --chart-5: 340 75% 55% - ; - --sidebar-background: 240 5.9% 10%; - --sidebar-foreground: 240 4.8% 95.9%; - --sidebar-primary: 224.3 76.3% 48%; - --sidebar-primary-foreground: 0 0% 100%; - --sidebar-accent: 240 3.7% 15.9%; - --sidebar-accent-foreground: 240 4.8% 95.9%; - --sidebar-border: 240 3.7% 15.9%; - --sidebar-ring: 217.2 91.2% 59.8%} -} -@layer base { - * { - @apply border-border; - } - body { - @apply bg-background text-foreground; - } -} +@import "tailwindcss"; +@import "tw-animate-css"; +@import "shadcn/tailwind.css"; -/* Add this in your global CSS file */ -.scrollbar-custom { - scrollbar-width: thin; -} +@custom-variant dark (&:is(.dark *)); -.scrollbar-custom::-webkit-scrollbar { - height: 4px; /* Adjust for horizontal scrollbars */ +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-sans); + --font-mono: var(--font-geist-mono); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --radius-2xl: calc(var(--radius) + 8px); + --radius-3xl: calc(var(--radius) + 12px); + --radius-4xl: calc(var(--radius) + 16px); } -.scrollbar-custom::-webkit-scrollbar-thumb { - border-radius: 4px; +:root { + --background: oklch(1 0 0); + --foreground: oklch(0.147 0.004 49.25); + --card: oklch(1 0 0); + --card-foreground: oklch(0.147 0.004 49.25); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.147 0.004 49.25); + --primary: oklch(0.67 0.16 58); + --primary-foreground: oklch(0.99 0.02 95); + --secondary: oklch(0.967 0.001 286.375); + --secondary-foreground: oklch(0.21 0.006 285.885); + --muted: oklch(0.97 0.001 106.424); + --muted-foreground: oklch(0.553 0.013 58.071); + --accent: oklch(0.67 0.16 58); + --accent-foreground: oklch(0.99 0.02 95); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.923 0.003 48.717); + --input: oklch(0.923 0.003 48.717); + --ring: oklch(0.709 0.01 56.259); + --chart-1: oklch(0.88 0.15 92); + --chart-2: oklch(0.77 0.16 70); + --chart-3: oklch(0.67 0.16 58); + --chart-4: oklch(0.56 0.15 49); + --chart-5: oklch(0.47 0.12 46); + --radius: 0.625rem; + --sidebar: oklch(0.985 0.001 106.423); + --sidebar-foreground: oklch(0.147 0.004 49.25); + --sidebar-primary: oklch(0.67 0.16 58); + --sidebar-primary-foreground: oklch(0.99 0.02 95); + --sidebar-accent: oklch(0.67 0.16 58); + --sidebar-accent-foreground: oklch(0.99 0.02 95); + --sidebar-border: oklch(0.923 0.003 48.717); + --sidebar-ring: oklch(0.709 0.01 56.259); } -@keyframes spin { - to { transform: rotate(360deg); } -} -.animate-spin { - animation: spin 1s linear infinite; -} - -@keyframes pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.5; } -} -.animate-pulse { - animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; +.dark { + --background: oklch(0.147 0.004 49.25); + --foreground: oklch(0.985 0.001 106.423); + --card: oklch(0.216 0.006 56.043); + --card-foreground: oklch(0.985 0.001 106.423); + --popover: oklch(0.216 0.006 56.043); + --popover-foreground: oklch(0.985 0.001 106.423); + --primary: oklch(0.77 0.16 70); + --primary-foreground: oklch(0.28 0.07 46); + --secondary: oklch(0.274 0.006 286.033); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.268 0.007 34.298); + --muted-foreground: oklch(0.709 0.01 56.259); + --accent: oklch(0.77 0.16 70); + --accent-foreground: oklch(0.28 0.07 46); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.553 0.013 58.071); + --chart-1: oklch(0.88 0.15 92); + --chart-2: oklch(0.77 0.16 70); + --chart-3: oklch(0.67 0.16 58); + --chart-4: oklch(0.56 0.15 49); + --chart-5: oklch(0.47 0.12 46); + --sidebar: oklch(0.216 0.006 56.043); + --sidebar-foreground: oklch(0.985 0.001 106.423); + --sidebar-primary: oklch(0.77 0.16 70); + --sidebar-primary-foreground: oklch(0.28 0.07 46); + --sidebar-accent: oklch(0.77 0.16 70); + --sidebar-accent-foreground: oklch(0.28 0.07 46); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.553 0.013 58.071); } @layer base { diff --git a/app/icon.png b/app/icon.png index f54b372..b631cdd 100644 Binary files a/app/icon.png and b/app/icon.png differ diff --git a/app/icon.svg b/app/icon.svg index 9b5ea15..9c7adf2 100644 --- a/app/icon.svg +++ b/app/icon.svg @@ -1,3 +1,8 @@ - \ No newline at end of file + + + + + + + \ No newline at end of file diff --git a/app/layout.tsx b/app/layout.tsx index e2f5e2e..4777f1d 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -3,8 +3,10 @@ import { ThemeProvider } from "@/components/theme-provider" import { Inter } from 'next/font/google'; import type { ReactNode } from 'react'; import type { Metadata } from "next"; -import { Toaster } from '@/components/ui/toaster'; import { mockServer } from '@/mocks/server'; +import { TooltipProvider } from "@/components/ui/tooltip" +import { Toaster } from "@/components/ui/sonner" + const inter = Inter({ subsets: ['latin'], @@ -46,9 +48,18 @@ export default async function Layout({ children }: { children: ReactNode }) { defaultTheme="dark" enableSystem disableTransitionOnChange - > - {children} - + > + {/* Background gradient */} +
+ + {/* Animated abstract shapes */} +
+
+
+
+ {children} + + diff --git a/app/login/page.tsx b/app/login/page.tsx index 1bb68cd..355fb7c 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -1,16 +1,20 @@ -import { authOptions } from "@/auth" -import { LoginForm } from "@/components/LoginForm/login-form" -import { getServerSession } from "next-auth" +import { AuthForm } from "@/components/Auth/AuthForm" +import { auth } from "@/lib/auth" +import { headers } from "next/headers" import { redirect } from 'next/navigation' export default async function Page() { - const session = await getServerSession(authOptions) - if (session) redirect("/") + const session = await auth.api.getSession({ + headers: await headers(), + }); + + if (session) redirect("/"); + return ( -
-
- +
+
+
- ) + ); } diff --git a/app/manifest.json b/app/manifest.json index ccf313a..e9ec274 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,6 +1,6 @@ { - "name": "MyWebSite", - "short_name": "MySite", + "name": "Dcup", + "short_name": "Dcup", "icons": [ { "src": "/web-app-manifest-192x192.png", diff --git a/app/opengraph-image.jpg b/app/opengraph-image.jpg index 4d0ea9a..fe09ca3 100644 Binary files a/app/opengraph-image.jpg and b/app/opengraph-image.jpg differ diff --git a/app/sitemap.ts b/app/sitemap.ts new file mode 100644 index 0000000..da681f6 --- /dev/null +++ b/app/sitemap.ts @@ -0,0 +1,24 @@ +import type { MetadataRoute } from 'next' + +export default function sitemap(): MetadataRoute.Sitemap { + return [ + { + url: 'https://dcup.dev/docs', + lastModified: new Date(), + changeFrequency: 'weekly', + priority: 1, + }, + { + url: 'https://dcup.dev/blog', + lastModified: new Date(), + changeFrequency: 'weekly', + priority: 1, + }, + { + url: 'https://dcup.dev', + lastModified: new Date(), + changeFrequency: 'monthly', + priority: 0.8, + }, + ] +} diff --git a/auth.ts b/auth.ts deleted file mode 100644 index c31f2ce..0000000 --- a/auth.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { NextAuthOptions } from "next-auth"; -import { DrizzleAdapter } from "@auth/drizzle-adapter"; -import GoogleProvider, { GoogleProfile } from "next-auth/providers/google"; -import GithubProvider, { GithubProfile } from "next-auth/providers/github"; -import { env } from "process"; -import { accounts, sessions, users, verificationTokens } from "./db/schemas/users"; -import { databaseDrizzle } from "./db"; - -const dcupEnv = env.DCUP_ENV - -export const authOptions: NextAuthOptions = { - secret: env.NEXTAUTH_SECRET, - session: { strategy: "jwt" }, - pages: { - signIn: "/login", - error: "/login", - }, - - adapter: DrizzleAdapter(databaseDrizzle, { - usersTable: users, - accountsTable: accounts, - sessionsTable: sessions, - verificationTokensTable: verificationTokens, - }), - callbacks: { - session: async ({ session, token }) => { - if (session) { - session.user.id = token.id as string; - session.user.name = token.name as string; - session.user.email = token.email as string; - session.user.image = token.picture as string - } - return session; - }, - jwt: async ({ user, token }) => { - if (user) { - token.id = user.id; - } - return token; - }, - }, - providers: [ - GoogleProvider({ - clientId: env.NEXT_PUBLIC_GOOGLE_CLIENT_ID!, - clientSecret: env.GOOGLE_CLIENT_SECRET!, - profile(profile: GoogleProfile) { - return { - id: profile.sub, - name: profile.name, - email: profile.email, - image: profile.picture, - plan: dcupEnv === 'CLOUD' ? 'FREE' : 'OS' - }; - }, - }), - GithubProvider({ - clientId: env.AUTH_GITHUB_ID!, - clientSecret: env.AUTH_GITHUB_SECRET!, - profile(profile: GithubProfile) { - return { - id: profile.id.toString(), - name: profile.name, - image: profile.avatar_url, - email: profile.email, - plan: dcupEnv === 'CLOUD' ? 'FREE' : 'OS' - }; - }, - }), - ] -} diff --git a/components.json b/components.json index 0843c32..add3af3 100644 --- a/components.json +++ b/components.json @@ -1,15 +1,17 @@ { "$schema": "https://ui.shadcn.com/schema.json", - "style": "new-york", + "style": "radix-lyra", "rsc": true, "tsx": true, "tailwind": { "config": "tailwind.config.js", "css": "app/global.css", - "baseColor": "neutral", + "baseColor": "stone", "cssVariables": true, "prefix": "" }, + "iconLibrary": "lucide", + "rtl": false, "aliases": { "components": "@/components", "utils": "@/lib/utils", @@ -17,5 +19,7 @@ "lib": "@/lib", "hooks": "@/hooks" }, - "iconLibrary": "lucide" -} \ No newline at end of file + "menuColor": "default", + "menuAccent": "bold", + "registries": {} +} diff --git a/components/Auth/AuthForm.tsx b/components/Auth/AuthForm.tsx new file mode 100644 index 0000000..52ae11e --- /dev/null +++ b/components/Auth/AuthForm.tsx @@ -0,0 +1,340 @@ +"use client"; + +import { useState, useTransition } from "react"; +import { useRouter } from "next/navigation"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import * as z from "zod"; +import { motion, AnimatePresence } from "framer-motion"; +import { authClient } from "@/lib/auth-client"; +import { FaGoogle, FaGithub } from "react-icons/fa"; +import { EyeIcon, EyeOffIcon } from "lucide-react"; + +// shadcn components +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Logo } from "../Logo/logo"; +import { Spinner } from "../ui/spinner"; +import { toast } from "sonner" +// Form schemas +const loginSchema = z.object({ + email: z.email("Invalid email address"), + password: z.string().min(1, "Password is required"), +}); + +const signupSchema = z.object({ + email: z.email("Invalid email address"), + password: z.string().min(8, "Password must be at least 8 characters"), + name: z.string().min(2, "Name must be at least 2 characters").optional(), +}); + +type LoginValues = z.infer; +type SignupValues = z.infer; + +// Animation variants +const fadeUp = { + hidden: { opacity: 0, y: 20 }, + visible: { opacity: 1, y: 0, transition: { duration: 0.5 } }, +}; + +const staggerChildren = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { + staggerChildren: 0.1, + }, + }, +}; + +export function AuthForm() { + const router = useRouter(); + const [isPending, startTransition] = useTransition(); + const [activeTab, setActiveTab] = useState<"login" | "signup">("login"); + const [showPassword, setShowPassword] = useState(false); + + // Login form + const { + register: registerLogin, + handleSubmit: handleLoginSubmit, + formState: { errors: loginErrors }, + } = useForm({ + resolver: zodResolver(loginSchema), + }); + + // Signup form + const { + register: registerSignup, + handleSubmit: handleSignupSubmit, + formState: { errors: signupErrors }, + } = useForm({ + resolver: zodResolver(signupSchema), + }); + + const onLogin = (data: LoginValues) => { + startTransition(async () => { + const { error } = await authClient.signIn.email({ + email: data.email, + password: data.password, + }); + if (error) { + toast.error(error.message || "unkown error") + return; + } + router.replace("/"); + }); + }; + + const onSignup = (data: SignupValues) => { + startTransition(async () => { + const { error } = await authClient.signUp.email({ + email: data.email, + password: data.password, + name: data.name!, + plan: process.env.DCUP_ENV === 'CLOUD' ? 'FREE' : 'OS' + }); + if (error) { + toast.error(error.message || "unkown error") + return; + } + router.replace("/"); + }); + }; + + const loginWith = (provider: "google" | "github") => { + startTransition(async () => { + const { error } = await authClient.signIn.social({ + provider, + }); + if (error) { + toast.error(error.message || "unkown error") + } + }); + }; + + + return ( + + + + + {/* Logo */} + + + + + + Dcup + + + Advanced RAG for Personal Knowledge + + + + + + setActiveTab(v as "login" | "signup")} className="w-full"> + + Login + Sign Up + + + + {activeTab === "login" && ( + + +
+
+ + + {loginErrors.email && ( +

{loginErrors.email.message}

+ )} +
+
+ +
+ + +
+ {loginErrors.password && ( +

{loginErrors.password.message}

+ )} +
+ +
+
+
+ )} + + {activeTab === "signup" && ( + + +
+
+ + + {signupErrors.name && ( +

{signupErrors.name.message}

+ )} +
+
+ + + {signupErrors.email && ( +

{signupErrors.email.message}

+ )} +
+
+ +
+ + +
+ {signupErrors.password && ( +

{signupErrors.password.message}

+ )} +
+ +
+
+
+ )} +
+
+ +
+
+ +
+
+ Or continue with +
+
+ + + + + + + + + +
+ + +

+ By continuing, you agree to our{" "} + {" "} + and{" "} + + . +

+
+
+
+
+ ); +} diff --git a/components/Avatar/UserAvatar.tsx b/components/Avatar/UserAvatar.tsx index 0a15a9d..273f89e 100644 --- a/components/Avatar/UserAvatar.tsx +++ b/components/Avatar/UserAvatar.tsx @@ -11,17 +11,17 @@ import { AvatarFallback, AvatarImage } from "@/components/ui/avatar" -import { Session } from "next-auth" import Link from "next/link"; +import { User } from "better-auth"; -export const UserAvatar = ({ session }: { session: Session }) => { +export const UserAvatar = ({ session }: { session: User }) => { return ( - - {session.user?.name?.charAt(0)} + + {session?.name?.charAt(0)} diff --git a/components/ConfigConnection/ConfigConnection.tsx b/components/ConfigConnection/ConfigConnection.tsx index 81d3823..a37586b 100644 --- a/components/ConfigConnection/ConfigConnection.tsx +++ b/components/ConfigConnection/ConfigConnection.tsx @@ -102,7 +102,7 @@ export const ConfigConnection = ({ connection, directory, status, open, setOpen, Configure - + setOpen(false)} className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"> Close diff --git a/components/Connections/Connections.tsx b/components/Connections/Connections.tsx index e23e67e..1c24e9f 100644 --- a/components/Connections/Connections.tsx +++ b/components/Connections/Connections.tsx @@ -1,10 +1,9 @@ "use client" -import { TableCell, TableRow } from "@/components/ui/table"; +import { TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { timeAgo } from "@/lib/utils"; import { useEffect, useState } from "react"; -import { ConnectionQuery, ConnectionToken } from "@/app/(protected)/connections/page"; import { getServiceIcon } from "@/lib/helepers"; -import { Check, X, Pickaxe, AlertCircle } from "lucide-react"; +import { Check, X, Pickaxe, AlertCircle, Table } from "lucide-react"; import { DataSource } from "@/DataSource"; import { ConnectionProgress } from "@/events"; @@ -20,18 +19,22 @@ import { TooltipTrigger, } from "@/components/ui/tooltip" import { useRouter } from "next/navigation"; +import RAG_Client from "@/Clients/RAG_Client"; +import { FiDatabase } from "react-icons/fi"; +import { ProcessedFilesTable } from "@/db/schema"; -export default function Connections({ connections, tokens }: { connections: ConnectionQuery[], tokens: ConnectionToken }) { +export default function Connections({ files, userId }: {files:ProcessedFilesTable[] ,userId: string }) { const [isMounted, setIsMounted] = useState(false) const route = useRouter() const [connProgress, setConnProgress] = useState(null); useEffect(() => { setIsMounted(true) - const eventSource = new EventSource("/api/progress") + console.log({ userId }) + const eventSource = new EventSource(RAG_Client.getProgressUrl(userId)) eventSource.onmessage = (event) => { - const data = JSON.parse(event.data) as ConnectionProgress - setConnProgress(data) + const data = JSON.parse(event.data) + console.log({ data }) } eventSource.onerror = () => { @@ -51,45 +54,65 @@ export default function Connections({ connections, tokens }: { connections: Conn return connections.map(connection => { const progress = connection.id === connProgress?.connectionId ? connProgress : null; - + if (connections.length === 0) return return ( - - -
- {getServiceIcon(connection.service)} - - {connection.service.toLowerCase().replace('_', ' ')} - -
-

- {connection.identifier} -

-
- {connection.folderName || 'Untitled'} - {progress?.processedFile ?? connection.files.length} - {progress?.processedPage ?? connection.files.reduce((sum, file) => sum + file.totalPages, 0)} - - {!isMounted ? "Loading..." : timeAgo(connection.createdAt)} - - - {!isMounted ? "Loading..." : } - - - - - - - - {progress?.errorMessage && - - - Error - - {progress?.errorMessage} - - - } -
) +
+

Active Connections

+
+ + + + Source + Directory + Documents + Pages + Date Added + Last Synced + + + + + +
+ {getServiceIcon(connection.service)} + + {connection.service.toLowerCase().replace('_', ' ')} + +
+

+ {connection.identifier} +

+
+ {connection.folderName || 'Untitled'} + {progress?.processedFile ?? connection.files.length} + {progress?.processedPage ?? connection.files.reduce((sum, file) => sum + file.totalPages, 0)} + + {!isMounted ? "Loading..." : timeAgo(connection.createdAt)} + + + {!isMounted ? "Loading..." : } + + + + + + + + {progress?.errorMessage && + + + Error + + {progress?.errorMessage} + + + } +
+
+
+
+
+ ) }); } @@ -112,3 +135,17 @@ const ConnectionStatus = ({ status, connection, errorMessage }: { status?: "PROC ) } + +function EmptyState() { + return ( +
+
+ +
+

No Connected Sources

+

+ Connect your first data source to start syncing documents and pages with your application. We support Google Drive, Notion, AWS, and more. +

+
+ ); +} diff --git a/components/FileTable/FileTable.tsx b/components/FileTable/FileTable.tsx new file mode 100644 index 0000000..8cb83cc --- /dev/null +++ b/components/FileTable/FileTable.tsx @@ -0,0 +1,26 @@ +"use client" +import RAG_Client from "@/Clients/RAG_Client" +import { useEffect } from "react" + +export const FileTable = ({ userId }: { userId: string }) => { + useEffect(() => { + console.log({ userId }) + const eventSource = new EventSource(RAG_Client.getProgressUrl(userId)) + eventSource.onmessage = (event) => { + const data = JSON.parse(event.data) + console.log({ data }) + } + + eventSource.onerror = () => { + eventSource.close() + } + + return () => eventSource.close() + + }, []) + return ( +
+

TABLE ..

+
+ ) +} diff --git a/components/LoginForm/login-form.tsx b/components/LoginForm/login-form.tsx deleted file mode 100644 index 9119b80..0000000 --- a/components/LoginForm/login-form.tsx +++ /dev/null @@ -1,97 +0,0 @@ -"use client" -import { cn } from "@/lib/utils" -import { Button } from "@/components/ui/button" -import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle -} from "@/components/ui/card"; -import { Loader } from "@/components/Loader/Loader"; -import { signIn } from "next-auth/react"; -import { FaGithub, FaGoogle } from "react-icons/fa"; -import { useTransition } from "react"; -import { useRouter, useSearchParams } from "next/navigation"; -import { useSignInErrorMessage } from "@/lib/errors/auth_hook"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import { AlertCircleIcon } from "lucide-react"; - -export function LoginForm({ - className, - ...props -}: React.ComponentPropsWithoutRef<"div">) { - const [isPending, startTransition] = useTransition(); - const router = useRouter() - const params = useSearchParams(); - const errorType = params.get("error"); - - const decodedErrorType = decodeURIComponent(errorType || ""); - const errorMessage = useSignInErrorMessage(decodedErrorType); - - const loginWith = (method: string) => { - startTransition(async () => { - const login = async () => { - try { - const res = await signIn(method) - if (res?.error) { - router.push(`/login?error=${encodeURIComponent(res.error)}`); - } else if (res?.ok) { - router.push("/"); - } - } catch { - router.push( - `/login?error=${encodeURIComponent("An unexpected error occurred")}`, - ); - } - return; - }; - await login(); - }); - }; - if (isPending) { - return ; - } - - - return ( -
- - {errorType && errorMessage && ( - - - SignIn Failed - {errorMessage} - - )} - - Login - - Welcome to Dcup - - - -
- - -
-
- -

- By continuing, you agree to our - -

-
-
-
- ) -} diff --git a/components/Logo/logo.tsx b/components/Logo/logo.tsx index 0192891..5b6a347 100644 --- a/components/Logo/logo.tsx +++ b/components/Logo/logo.tsx @@ -1,20 +1,20 @@ import Image from "next/image"; import Link from "next/link"; -export const Logo = () => { +export const Logo = ({ size = 45, withName }: { size?: number, withName?: boolean }) => { return ( -
+
-
+
{/* Light Mode Logo */} D-Cup Logo Light @@ -22,15 +22,15 @@ export const Logo = () => { D-Cup Logo Dark
-

+ {withName &&

Dcup -

+ }
); diff --git a/components/UploadFileForm/UploadFileForm.tsx b/components/UploadFileForm/UploadFileForm.tsx index b57ce51..a613aa3 100644 --- a/components/UploadFileForm/UploadFileForm.tsx +++ b/components/UploadFileForm/UploadFileForm.tsx @@ -1,131 +1,162 @@ -import { Button } from "@/components/ui/button" -import { Label } from "@/components/ui/label" -import { AlertCircle, Link, Loader2, UploadCloud, XCircleIcon } from "lucide-react" -import { EMPTY_FORM_STATE } from "@/lib/zodErrorHandle" -import { toast } from "@/hooks/use-toast" -import { Input } from "@/components/ui/input" -import { Textarea } from "@/components/ui/textarea" -import { - DialogFooter, -} from "@/components/ui/dialog" +"use client"; + +import { useState, useTransition, useCallback } from "react"; +import { FileRejection, useDropzone } from "react-dropzone"; +import { motion, AnimatePresence } from "framer-motion"; import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card" + UploadCloud, + Link, + X, + FileText, + FileSpreadsheet, + FileJson, + File, + AlertCircle, + Loader2, + Plus, + Trash2, +} from "lucide-react"; +import { toast } from "@/hooks/use-toast"; + +// shadcn components +import { Button } from "@/components/ui/button"; import { - Tabs, - TabsContent, - TabsList, - TabsTrigger, -} from "@/components/ui/tabs" -import { setConnectionConfig } from "@/actions/connctions/set" -import { ChangeEvent, Dispatch, SetStateAction, useMemo, useRef, useState, useTransition } from "react" -import { ConnectionQuery } from "@/app/(protected)/connections/page" - -type TFileForm = { - setOpen: Dispatch>; - connection?: ConnectionQuery; -}; - -export const UploadFileForm = ({ setOpen, connection }: TFileForm) => { + DialogFooter, + DialogHeader, + DialogTitle, + DialogDescription, +} from "@/components/ui/dialog"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Separator } from "@/components/ui/separator"; +import { newFilesAction } from "@/actions/files/new"; +import { EMPTY_FORM_STATE } from "@/lib/zodErrorHandle"; + +const SUPPORTED_MIME_TYPES = new Map([ + ["application/pdf", { ext: "pdf", icon: FileText }], + ["text/csv", { ext: "csv", icon: FileSpreadsheet }], + ["application/vnd.ms-excel", { ext: "xls", icon: FileSpreadsheet }], + ["application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", { ext: "xlsx", icon: FileSpreadsheet }], + ["text/markdown", { ext: "md", icon: FileText }], + ["application/json", { ext: "json", icon: FileJson }], +]); + +const SUPPORTED_EXTENSIONS = Array.from(SUPPORTED_MIME_TYPES.values()).map(v => v.ext); +const URL_REGEX = /^(https?:\/\/)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(\/.*)?$/; + + +interface UploadFileFormEnhancedProps { + setOpen: React.Dispatch>; + currentFiles?: string[]; +} + +export function UploadFileForm({ setOpen, currentFiles = [] }: UploadFileFormEnhancedProps) { const [links, setLinks] = useState([]); - const [text, setText] = useState("") const [files, setFiles] = useState([]); const [removedFiles, setRemovedFiles] = useState([]); + const [metadata, setMetadata] = useState>([ + { key: "", value: "" }, + ]); const [pending, startTransition] = useTransition(); - const handleUploadFiles = async (data: FormData) => { - links.forEach((link) => data.append("links", link)); - files.forEach((file) => data.append("files", file)); + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + // Filter out empty metadata fields + const filteredMetadata = metadata.reduce((acc, meta) => + meta.key.trim() && meta.value.trim() ? { ...acc, [meta.key]: meta.value } : acc + , {}); + const metadataJson = JSON.stringify(filteredMetadata); + + const formData = new FormData(); + links.forEach(link => formData.append("links", link)); + files.forEach(file => formData.append("files", file)); + removedFiles.forEach(name => formData.append("removedFiles", name)); + formData.append("metadata", metadataJson); + startTransition(async () => { try { - if (connection) { - data.set("service", "DIRECT_UPLOAD_UPDATE"); - data.set("connectionId", connection.id) - removedFiles.forEach((fileName) => data.append("removedFiles", fileName)); - } else { - data.set("service", "DIRECT_UPLOAD"); - if (text) data.append("texts", text) - } - const res = await setConnectionConfig(EMPTY_FORM_STATE, data) + const res = await newFilesAction(EMPTY_FORM_STATE, formData) if (res.status === "SUCCESS") { setLinks([]); setFiles([]); - setRemovedFiles([]); // Reset removedFiles on success + setRemovedFiles([]); setOpen(false); - } - if (res.message) { - toast({ title: res.message }); + toast({ title: "Upload successful", variant: "default" }); } if (res.status === "ERROR") { throw new Error(res.message); } } catch (err: any) { - toast({ - title: err.message, - variant: "destructive", - }); + toast({ title: err.message || "Upload failed", variant: "destructive" }); } }); }; return ( -
-
- - -
+ + + Upload Knowledge + + Add files or links to your knowledge base. Supported formats: PDF, CSV, Excel, JSON, Markdown. + + -
- -