diff --git a/.gitignore b/.gitignore index dd019e403..6705d106d 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,4 @@ yarn-error.log* .vercel .vscode .env*.local +.history \ No newline at end of file diff --git a/README.md b/README.md index d55369c6a..8f7bbf120 100644 --- a/README.md +++ b/README.md @@ -59,3 +59,22 @@ pnpm dev ``` Your app template should now be running on [localhost:3000](http://localhost:3000/). + +## PSQL Helper queries + +- Drop all tables + ``` + DO $$ + BEGIN + EXECUTE ( + SELECT string_agg('DROP TABLE IF EXISTS "' || tablename || '" CASCADE;', ' ') + FROM pg_tables + WHERE schemaname = 'public' + ); + END $$; + ``` + +- Drop Drizzle schema + ``` + DROP SCHEMA drizzle CASCADE; + ``` \ No newline at end of file diff --git a/app/(chat)/layout.tsx b/app/(chat)/layout.tsx index 981aa505b..a79cf8c01 100644 --- a/app/(chat)/layout.tsx +++ b/app/(chat)/layout.tsx @@ -13,7 +13,7 @@ export default async function Layout({ children: React.ReactNode; }) { const [session, cookieStore] = await Promise.all([auth(), cookies()]); - const isCollapsed = cookieStore.get('sidebar:state')?.value !== 'true'; + const isCollapsed = cookieStore.get('sidebar:state')?.value !== 'false'; return ( diff --git a/app/(settings)/settings/configuration/index.tsx b/app/(settings)/settings/configuration/index.tsx new file mode 100644 index 000000000..2e32a2699 --- /dev/null +++ b/app/(settings)/settings/configuration/index.tsx @@ -0,0 +1,124 @@ +// Configuration.tsx + +'use client'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { motion, AnimatePresence } from 'framer-motion'; +import { useState } from 'react'; +import { useForm } from 'react-hook-form'; + +import { Button } from '@/components/ui/button'; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from '@/components/ui/card'; + +import { StepForm } from './step-form'; +import { Stepper } from './stepper'; +import { steps } from './steps'; +import { configurationSchema, ConfigurationFormData } from './validation'; + +interface ConfigurationCarouselProps { + onClose: () => void; +} + +export function Configuration({ onClose }: ConfigurationCarouselProps) { + const [currentStep, setCurrentStep] = useState(0); + const [visitedSteps, setVisitedSteps] = useState([0]); + const [isSetupCompleted, setIsSetupCompleted] = useState(false); + + const formMethods = useForm({ + resolver: zodResolver(configurationSchema), + mode: 'onChange', + }); + + const { getValues, trigger } = formMethods; + + const handleNextStep = async () => { + const fields = Object.keys(steps[currentStep].options) as Array< + keyof ConfigurationFormData + >; + const isValid = await trigger(fields); + + if (isValid) { + if (currentStep < steps.length - 1) { + const nextStep = currentStep + 1; + setCurrentStep(nextStep); + setVisitedSteps((prev) => [...new Set([...prev, nextStep])]); + } else { + setIsSetupCompleted(true); + } + } + }; + + const handlePreviousStep = () => { + if (currentStep > 0) { + setCurrentStep(currentStep - 1); + } + }; + + const isStepComplete = (step: number) => { + const fields = Object.keys(steps[step].options) as Array< + keyof ConfigurationFormData + >; + const values = getValues(); + return fields.every((key) => values[key]); + }; + + const allStepsCompleted = steps.every((_, index) => isStepComplete(index)); + + return ( + + + + + Configuration Setup + + + Complete the following steps to configure your system + + + + { + setCurrentStep(step); + setVisitedSteps((prev) => [...new Set([...prev, step])]); + }} + isStepComplete={isStepComplete} + /> + + + + + + + + + + + + ); +} diff --git a/app/(settings)/settings/configuration/step-form.tsx b/app/(settings)/settings/configuration/step-form.tsx new file mode 100644 index 000000000..7d9fb5908 --- /dev/null +++ b/app/(settings)/settings/configuration/step-form.tsx @@ -0,0 +1,113 @@ +// StepForm.tsx + +import { motion } from 'framer-motion'; +import { ArrowLeft, ArrowRight, Check } from 'lucide-react'; +import { Controller, UseFormReturn } from 'react-hook-form'; + +import { Button } from '@/components/ui/button'; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from '@/components/ui/card'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; + +import { steps } from './steps'; +import { ConfigurationFormData } from './validation'; + +interface StepFormProps { + currentStep: number; + formMethods: UseFormReturn; + handleNextStep: () => void; + handlePreviousStep: () => void; +} + +export const StepForm = ({ + currentStep, + formMethods, + handleNextStep, + handlePreviousStep, +}: StepFormProps) => { + const { control, formState } = formMethods; + return ( + + + + {steps[currentStep].title} + {steps[currentStep].description} + + + {Object.entries(steps[currentStep].options).map(([key, options]) => ( +
+ + ( + + )} + /> + {formState.errors[key as keyof ConfigurationFormData] && ( +

+ { + formState.errors[key as keyof ConfigurationFormData] + ?.message + } +

+ )} +
+ ))} +
+ + + + +
+
+ ); +}; diff --git a/app/(settings)/settings/configuration/stepper.tsx b/app/(settings)/settings/configuration/stepper.tsx new file mode 100644 index 000000000..1d000cb6d --- /dev/null +++ b/app/(settings)/settings/configuration/stepper.tsx @@ -0,0 +1,53 @@ +// Stepper.tsx + +import { motion } from 'framer-motion'; + +import { steps } from './steps'; + +interface StepperProps { + currentStep: number; + setCurrentStep: (step: number) => void; + isStepComplete: (step: number) => boolean; +} + +export const Stepper = ({ + currentStep, + setCurrentStep, + isStepComplete, +}: StepperProps) => { + return ( +
+ {steps.map((step, index) => { + const StepIcon = step.icon; + return ( +
+ setCurrentStep(index)} + > + + + {index < steps.length - 1 && ( + + )} +
+ ); + })} +
+ ); +}; diff --git a/app/(settings)/settings/configuration/steps.tsx b/app/(settings)/settings/configuration/steps.tsx new file mode 100644 index 000000000..8fe15da56 --- /dev/null +++ b/app/(settings)/settings/configuration/steps.tsx @@ -0,0 +1,45 @@ +// steps.ts + +import { Database, FileText, Settings, Zap } from 'lucide-react'; + +export const steps = [ + { + title: 'Set up your providers', + description: + 'Configure processing/chunking, vector DB, reranking, and embedding', + icon: Database, + options: { + processing: ['Option 1', 'Option 2', 'Option 3'], + vectorDB: ['Option A', 'Option B', 'Option C'], + reranking: ['Choice 1', 'Choice 2', 'Choice 3'], + embedding: ['Type 1', 'Type 2', 'Type 3'], + }, + }, + { + title: 'Configure your processing settings', + description: 'Set up the processing parameters for your pipeline', + icon: FileText, + options: { + parameter1: ['Low', 'Medium', 'High'], + parameter2: ['Fast', 'Balanced', 'Thorough'], + }, + }, + { + title: 'Configure your chunking settings', + description: 'Set up the chunking parameters for your pipeline', + icon: Settings, + options: { + chunkSize: ['Small', 'Medium', 'Large'], + overlap: ['None', 'Minimal', 'Moderate', 'Significant'], + }, + }, + { + title: 'Configure your retrieval settings', + description: 'Set up the retrieval parameters for your pipeline', + icon: Zap, + options: { + method: ['BM25', 'TF-IDF', 'Semantic'], + topK: ['5', '10', '20', '50'], + }, + }, +]; diff --git a/app/(settings)/settings/configuration/validation.tsx b/app/(settings)/settings/configuration/validation.tsx new file mode 100644 index 000000000..6e7fbaaec --- /dev/null +++ b/app/(settings)/settings/configuration/validation.tsx @@ -0,0 +1,18 @@ +// validation.ts + +import { z } from 'zod'; + +export const configurationSchema = z.object({ + processing: z.string().min(1, { message: 'Processing is required' }), + vectorDB: z.string().min(1, { message: 'Vector DB is required' }), + reranking: z.string().min(1, { message: 'Reranking is required' }), + embedding: z.string().min(1, { message: 'Embedding is required' }), + parameter1: z.string().min(1, { message: 'Parameter 1 is required' }), + parameter2: z.string().min(1, { message: 'Parameter 2 is required' }), + chunkSize: z.string().min(1, { message: 'Chunk size is required' }), + overlap: z.string().min(1, { message: 'Overlap is required' }), + method: z.string().min(1, { message: 'Method is required' }), + topK: z.string().min(1, { message: 'Top K is required' }), +}); + +export type ConfigurationFormData = z.infer; diff --git a/app/(settings)/settings/knowledgebase/index.tsx b/app/(settings)/settings/knowledgebase/index.tsx new file mode 100644 index 000000000..cac63fd4e --- /dev/null +++ b/app/(settings)/settings/knowledgebase/index.tsx @@ -0,0 +1,490 @@ +'use client'; + +import { motion, AnimatePresence } from 'framer-motion'; +import { + Upload, + Trash2, + RefreshCw, + ArrowUpDown, + Search, + ArrowLeft, + CheckCircle, + XCircle, + Clock, + ChevronLeft, + ChevronRight, +} from 'lucide-react'; +import { useState, useEffect, useRef } from 'react'; + +import { Button } from '@/components/ui/button'; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Progress } from '@/components/ui/progress'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@/components/ui/tooltip'; + +interface File { + id: string; + name: string; + size: number; + type: string; + uploadDate: Date; + status: 'uploading' | 'uploaded' | 'processing' | 'processed' | 'error'; + progress: number; +} + +interface KnowledgebaseProps { + onClose: () => void; +} + +export function Knowledgebase({ onClose }: KnowledgebaseProps) { + const fileInputRef = useRef(null); + const [files, setFiles] = useState([]); + const [searchTerm, setSearchTerm] = useState(''); + const [sortColumn, setSortColumn] = useState('uploadDate'); + const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc'); + const [isUploading, setIsUploading] = useState(false); + const [canReturnToSettings, setCanReturnToSettings] = useState(false); + const [currentPage, setCurrentPage] = useState(1); + const filesPerPage = 5; + + useEffect(() => { + const hasProcessedFile = files.some((file) => file.status === 'processed'); + setCanReturnToSettings(hasProcessedFile); + }, [files]); + + const simulateFileUpload = (file: File) => { + const uploadInterval = setInterval(() => { + setFiles((prevFiles) => + prevFiles.map((f) => + f.id === file.id + ? { ...f, progress: Math.min(f.progress + 10, 100) } + : f + ) + ); + }, 200); + + setTimeout( + () => { + clearInterval(uploadInterval); + setFiles((prevFiles) => + prevFiles.map((f) => + f.id === file.id ? { ...f, status: 'uploaded', progress: 100 } : f + ) + ); + }, + 2000 + Math.random() * 1000 + ); + }; + + const handleFileUpload = (event: React.ChangeEvent) => { + const fileList = event.target.files; + if (fileList) { + setIsUploading(true); + const newFiles: File[] = Array.from(fileList).map((file) => ({ + id: Math.random().toString(36).substr(2, 9), + name: file.name, + size: file.size, + type: file.type, + uploadDate: new Date(), + status: 'uploading', + progress: 0, + })); + + setFiles((prevFiles) => [...prevFiles, ...newFiles]); + newFiles.forEach(simulateFileUpload); + + setTimeout( + () => { + setIsUploading(false); + }, + 2000 + newFiles.length * 500 + ); + } + }; + + const handleProcessFile = (id: string) => { + setFiles((prevFiles) => + prevFiles.map((file) => + file.id === id ? { ...file, status: 'processing', progress: 0 } : file + ) + ); + + const processInterval = setInterval(() => { + setFiles((prevFiles) => + prevFiles.map((file) => + file.id === id + ? { ...file, progress: Math.min(file.progress + 10, 100) } + : file + ) + ); + }, 200); + + setTimeout( + () => { + clearInterval(processInterval); + setFiles((prevFiles) => + prevFiles.map((file) => + file.id === id + ? { ...file, status: 'processed', progress: 100 } + : file + ) + ); + }, + 2000 + Math.random() * 1000 + ); + }; + + const handleDeleteFile = (id: string) => { + setFiles((prevFiles) => prevFiles.filter((file) => file.id !== id)); + }; + + const handleSort = (column: keyof File) => { + if (column === sortColumn) { + setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc'); + } else { + setSortColumn(column); + setSortDirection('asc'); + } + }; + + const sortedFiles = [...files].sort((a, b) => { + if (a[sortColumn] < b[sortColumn]) return sortDirection === 'asc' ? -1 : 1; + if (a[sortColumn] > b[sortColumn]) return sortDirection === 'asc' ? 1 : -1; + return 0; + }); + + const filteredFiles = sortedFiles.filter((file) => + file.name.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + const totalPages = + filteredFiles.length > 0 + ? Math.ceil(filteredFiles.length / filesPerPage) + : 0; + const paginatedFiles = filteredFiles.slice( + (currentPage - 1) * filesPerPage, + currentPage * filesPerPage + ); + + const emptyRowsCount = Math.max(0, filesPerPage - paginatedFiles.length); + + const formatFileSize = (bytes: number) => { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + }; + + const getStatusIcon = (status: File['status'], progress: number) => { + switch (status) { + case 'uploading': + case 'processing': + return ( +
+ + + + +
+ ); + case 'uploaded': + return ; + case 'processed': + return ; + case 'error': + return ; + default: + return null; + } + }; + + return ( + + + + File Management + + Upload and manage your knowledgebase files + + + +
+
+ setSearchTerm(e.target.value)} + className="w-full" + /> +
+
+ + + +
+
+ + + + + handleSort('name')} + className="cursor-pointer" + > + Name + + handleSort('size')} + className="cursor-pointer hidden sm:table-cell" + > + Size + + handleSort('type')} + className="cursor-pointer hidden md:table-cell" + > + Type + + handleSort('uploadDate')} + className="cursor-pointer hidden lg:table-cell" + > + Upload Date{' '} + + + handleSort('status')} + className="cursor-pointer" + > + Status + + Actions + + + + {paginatedFiles.map((file) => ( + + + + + + {file.name} + + + {file.name} + + + + + + {formatFileSize(file.size)} + + + + + + {file.type.length > 10 + ? `${file.type.slice(0, 10)}…` + : file.type} + + + {file.type} + + + + + + {file.uploadDate.toLocaleString()} + + + + +
+ {getStatusIcon(file.status, file.progress)} + + {file.status} + +
+ + {file.status === 'uploading' || + file.status === 'processing' + ? `${file.progress}% complete` + : `${file.status.charAt(0).toUpperCase() + file.status.slice(1)}`} + +
+
+
+ + +
+ + +
+
+
+ ))} + {Array.from({ length: emptyRowsCount }).map((_, index) => ( + + + {/* Empty content, but same structure */} + Placeholder + + + Placeholder + + + Placeholder + + + Placeholder + + +
+ {/* Use invisible icons to maintain height */} + + Placeholder +
+
+ +
+ + +
+
+
+ ))} +
+
+
+ +
{files.length} file(s) uploaded
+ {totalPages > 0 && ( +
+ + + Page {currentPage} of {totalPages} + + +
+ )} +
+
+
+ ); +} diff --git a/app/(settings)/settings/page.tsx b/app/(settings)/settings/page.tsx new file mode 100644 index 000000000..3104f95cf --- /dev/null +++ b/app/(settings)/settings/page.tsx @@ -0,0 +1,233 @@ +'use client'; + +import { motion, AnimatePresence } from 'framer-motion'; +import { + Settings, + Anvil, + User, + CheckCircle, + MessageSquare, +} from 'lucide-react'; +import Link from 'next/link'; +import { useRouter } from 'next/navigation'; +import { useEffect, useState } from 'react'; + +import { Button } from '@/components/ui/button'; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from '@/components/ui/card'; +import { Progress } from '@/components/ui/progress'; + +import { Configuration } from './configuration'; +import { Knowledgebase } from './knowledgebase'; +import { Profile } from './profile'; + +export default function Component() { + const router = useRouter(); + const [isConfigurationComplete, setIsConfigurationComplete] = useState(false); + const [isKnowledgebaseComplete, setIsKnowledgebaseComplete] = useState(false); + const [isPersonalizationComplete, setIsPersonalizationComplete] = + useState(false); + const [progress, setProgress] = useState(0); + const [showChatButton, setShowChatButton] = useState(false); + const [showSteps, setShowSteps] = useState(true); + const [showConfigurationCarousel, setShowConfigurationCarousel] = + useState(false); + const [showKnowledgebaseCarousel, setShowKnowledgebaseCarousel] = + useState(false); + const [showProfileCustomization, setShowProfileCustomization] = + useState(false); + + // Setup steps + const setupSteps = [ + { + title: 'Configuration', + icon: Settings, + route: '/configuration', + isComplete: isConfigurationComplete, + disabled: false, + }, + { + title: 'Knowledgebase', + icon: Anvil, + route: '/knowledgebase', + isComplete: isKnowledgebaseComplete, + disabled: !isConfigurationComplete, + }, + { + title: 'Profile', + icon: User, + route: '/profile', + isComplete: isPersonalizationComplete, + disabled: !isKnowledgebaseComplete, + }, + ]; + + useEffect(() => { + let completedSteps = 0; + if (isConfigurationComplete) completedSteps++; + if (isKnowledgebaseComplete) completedSteps++; + if (isPersonalizationComplete) completedSteps++; + + setProgress((completedSteps / 3) * 100); + + if (completedSteps === 3) { + setTimeout(() => { + setShowSteps(false); + setShowChatButton(true); + }, 500); + } + }, [ + isConfigurationComplete, + isKnowledgebaseComplete, + isPersonalizationComplete, + ]); + + const handleRouteNavigation = (route: string) => { + if (route === '/configuration') { + setShowConfigurationCarousel(true); + } else if (route === '/knowledgebase') { + setShowKnowledgebaseCarousel(true); + } else if (route === '/profile') { + setShowProfileCustomization(true); + } else { + router.push(route); + } + }; + + const handleCloseConfigurationCarousel = () => { + setShowConfigurationCarousel(false); + setIsConfigurationComplete(true); + }; + + const handleCloseKnowledgebaseCarousel = () => { + setShowKnowledgebaseCarousel(false); + setIsKnowledgebaseComplete(true); + }; + + const handleCloseProfileCustomization = () => { + setShowProfileCustomization(false); + setIsPersonalizationComplete(true); + }; + + return ( +
+ + + + Welcome to the Forge + + + Complete the following steps to get started + + + + +
+ + {showSteps && + setupSteps.map((item, index) => ( + + + + ))} + + + + {showChatButton && ( + + + + + + )} + +
+
+ + + + + +
+ + + {showConfigurationCarousel && ( + + )} + + + + {showKnowledgebaseCarousel && ( + + )} + + + + {showProfileCustomization && ( + + )} + +
+ ); +} diff --git a/app/(settings)/settings/profile/index.tsx b/app/(settings)/settings/profile/index.tsx new file mode 100644 index 000000000..ec0dffa8e --- /dev/null +++ b/app/(settings)/settings/profile/index.tsx @@ -0,0 +1,180 @@ +'use client'; + +import { motion } from 'framer-motion'; +import { User, Calendar, Globe } from 'lucide-react'; +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; + +import { Button } from '@/components/ui/button'; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { Switch } from '@/components/ui/switch'; + +interface ProfileProps { + onClose: () => void; +} + +export function Profile({ onClose }: ProfileProps) { + const router = useRouter(); + + // State variables for form fields + const [name, setName] = useState(''); + const [gender, setGender] = useState('male'); + const [dateOfBirth, setDateOfBirth] = useState(''); + const [language, setLanguage] = useState(''); + const [personalizedResponses, setPersonalizedResponses] = useState(false); + + return ( +
+ + + + Personalize Your AI Assistant + + + Customize your experience by providing some information about + yourself + + + +
+ +
+ + setName(e.target.value)} /> +
+
+ + +
+ + +
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+ + setDateOfBirth(e.target.value)} + /> +
+
+ + +
+ + +
+
+ + +
+
+ +
+ Enable personalized responses based on your preferences. +
+
+ +
+
+
+
+ + + + + +
+
+ ); +} diff --git a/app/layout.tsx b/app/layout.tsx index d0d4c4577..b73c07a0d 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -2,6 +2,7 @@ import type { Metadata } from 'next'; import { Toaster } from 'sonner'; import { ThemeProvider } from '@/components/theme-provider'; +import { SpeedInsights } from "@vercel/speed-insights/next" import './globals.css'; @@ -57,7 +58,7 @@ export default async function RootLayout({ }} /> - + {children} + ); diff --git a/components/query-provider.tsx b/components/query-provider.tsx new file mode 100644 index 000000000..4e785af89 --- /dev/null +++ b/components/query-provider.tsx @@ -0,0 +1,20 @@ +'use client'; + +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; +import { useState } from 'react'; + +export function ReactQueryProvider({ + children, +}: { + children: React.ReactNode; +}) { + const [queryClient] = useState(() => new QueryClient()); + + return ( + + {children} + + + ); +} diff --git a/components/sidebar-user-nav.tsx b/components/sidebar-user-nav.tsx index cb41ec76c..79aa25a8e 100644 --- a/components/sidebar-user-nav.tsx +++ b/components/sidebar-user-nav.tsx @@ -67,4 +67,4 @@ export function SidebarUserNav({ user }: { user: User }) { ); -} +} \ No newline at end of file diff --git a/components/ui/button.tsx b/components/ui/button.tsx index c499e5dcb..a279bc3c2 100644 --- a/components/ui/button.tsx +++ b/components/ui/button.tsx @@ -5,7 +5,7 @@ import { cva, type VariantProps } from 'class-variance-authority'; import { cn } from '@/lib/utils'; const buttonVariants = cva( - 'inline-flex items-center gap-2 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 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', + 'inline-flex items-center justify-center gap-2 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 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', { variants: { variant: { diff --git a/components/ui/dialog.tsx b/components/ui/dialog.tsx new file mode 100644 index 000000000..01ff19c7e --- /dev/null +++ b/components/ui/dialog.tsx @@ -0,0 +1,122 @@ +"use client" + +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { X } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/components/ui/form.tsx b/components/ui/form.tsx new file mode 100644 index 000000000..ce264aef2 --- /dev/null +++ b/components/ui/form.tsx @@ -0,0 +1,178 @@ +"use client" + +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { Slot } from "@radix-ui/react-slot" +import { + Controller, + ControllerProps, + FieldPath, + FieldValues, + FormProvider, + useFormContext, +} from "react-hook-form" + +import { cn } from "@/lib/utils" +import { Label } from "@/components/ui/label" + +const Form = FormProvider + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +> = { + name: TName +} + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue +) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext) + const itemContext = React.useContext(FormItemContext) + const { getFieldState, formState } = useFormContext() + + const fieldState = getFieldState(fieldContext.name, formState) + + if (!fieldContext) { + throw new Error("useFormField should be used within ") + } + + const { id } = itemContext + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + } +} + +type FormItemContextValue = { + id: string +} + +const FormItemContext = React.createContext( + {} as FormItemContextValue +) + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId() + + return ( + +
+ + ) +}) +FormItem.displayName = "FormItem" + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField() + + return ( +