feat: add expert search, legal search and UI improvements

This commit is contained in:
Aktraiser 2024-12-30 13:34:26 +01:00
parent 2c5ca94b3c
commit 271199c527
53 changed files with 4595 additions and 708 deletions

View file

@ -38,8 +38,11 @@ const EmptyChat = ({
</div>
<div className="flex flex-col items-center justify-center min-h-screen max-w-screen-sm mx-auto p-2 space-y-8">
<h2 className="text-black/70 dark:text-white/70 text-3xl font-medium -mt-8">
Research begins here.
Ici c&apos;est vous le patron.
</h2>
<h3 className="text-black/70 dark:text-white/70 font-medium -mt-8">
Posez des questions, recherchez un expert pour répondre à vos besoins entrepreneuriaux
</h3>
<EmptyChatMessageInput
sendMessage={sendMessage}
focusMode={focusMode}

View file

@ -80,7 +80,7 @@ const EmptyChatMessageInput = ({
onChange={(e) => setMessage(e.target.value)}
minRows={2}
className="bg-transparent placeholder:text-black/50 dark:placeholder:text-white/50 text-sm text-black dark:text-white resize-none focus:outline-none w-full max-h-24 lg:max-h-36 xl:max-h-48"
placeholder="Ask anything..."
placeholder="Posez votre question..."
/>
<div className="flex flex-row items-center justify-between mt-4">
<div className="flex flex-row items-center space-x-2 lg:space-x-4">

View file

@ -0,0 +1,158 @@
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Expert, Location } from "@/types"; // Ajustez le chemin selon votre structure
import { Dispatch, SetStateAction } from 'react'; // Ajout de l'import
interface Expertise {
id: string;
name: string;
}
interface FilterModalProps {
open: boolean;
setOpen: (open: boolean) => void;
selectedPays: string;
setSelectedPays: (pays: string) => void;
selectedVille: string;
setSelectedVille: (ville: string) => void;
selectedExpertises: string[];
setSelectedExpertises: Dispatch<SetStateAction<string[]>>; // Correction du type
locations: Location[];
experts: Expert[] | null;
}
export const FilterModal = ({
open,
setOpen,
selectedPays,
setSelectedPays,
selectedVille,
setSelectedVille,
selectedExpertises,
setSelectedExpertises,
locations,
experts,
}: FilterModalProps) => {
const activeFiltersCount = [
...(selectedExpertises.length > 0 ? [1] : []),
selectedPays,
selectedVille
].filter(Boolean).length;
const expertises: Expertise[] = [
{ id: 'immobilier', name: 'Immobilier' },
{ id: 'finance', name: 'Finance' },
{ id: 'droit', name: 'Droit' },
{ id: 'fiscalite', name: 'Fiscalité' },
{ id: 'assurance', name: 'Assurance' },
{ id: 'patrimoine', name: 'Patrimoine' },
];
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="max-h-[90vh] h-[90vh] sm:h-auto sm:max-w-[425px]">
<DialogHeader>
<DialogTitle className="flex justify-between">
Filtres
<span className="text-sm text-muted-foreground">
{activeFiltersCount} filtre{activeFiltersCount > 1 ? 's' : ''} actif{activeFiltersCount > 1 ? 's' : ''}
</span>
</DialogTitle>
<DialogDescription>
Filtrez les experts par expertise et localisation
</DialogDescription>
</DialogHeader>
<div className="space-y-6 py-4">
{/* Section Expertises */}
<div>
<h3 className="font-medium mb-3">Expertises</h3>
<div className="flex flex-wrap gap-2">
{expertises.map((expertise) => (
<button
key={expertise.id}
onClick={() => setSelectedExpertises(prev =>
prev.includes(expertise.id)
? prev.filter(id => id !== expertise.id)
: [...prev, expertise.id]
)}
className={`px-3 py-1.5 rounded-full text-sm transition-colors
${selectedExpertises.includes(expertise.id)
? 'bg-blue-500 text-white'
: 'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'
}`}
>
{expertise.name}
</button>
))}
</div>
</div>
{/* Section Pays */}
<div>
<h3 className="font-medium mb-3">Pays</h3>
<div className="flex flex-wrap gap-2">
{locations.map(({ pays }) => (
<button
key={pays}
onClick={() => setSelectedPays(selectedPays === pays ? '' : pays)}
className={`px-3 py-1.5 rounded-full text-sm transition-colors
${selectedPays === pays
? 'bg-blue-500 text-white'
: 'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'
}`}
>
{pays}
</button>
))}
</div>
</div>
{/* Section Villes (conditionnelle) */}
{selectedPays && (
<div>
<h3 className="font-medium mb-3">Villes {selectedPays && `(${selectedPays})`}</h3>
<div className="flex flex-wrap gap-2">
{locations
.find(loc => loc.pays === selectedPays)
?.villes.map(ville => (
<button
key={ville}
onClick={() => setSelectedVille(selectedVille === ville ? '' : ville)}
className={`px-3 py-1.5 rounded-full text-sm transition-colors
${selectedVille === ville
? 'bg-blue-500 text-white'
: 'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'
}`}
>
{ville}
</button>
))}
</div>
</div>
)}
</div>
<div className="p-6 pt-4 flex gap-4 border-t mt-auto">
<Button
variant="outline"
className="flex-1"
onClick={() => {
setSelectedPays('');
setSelectedVille('');
setSelectedExpertises([]);
}}
>
Réinitialiser
</Button>
<Button
className="flex-1"
onClick={() => setOpen(false)}
>
Appliquer
</Button>
</div>
</DialogContent>
</Dialog>
);
};

View file

@ -0,0 +1,200 @@
/* eslint-disable @next/next/no-img-element */
import { BookCopy, PlusIcon } from 'lucide-react';
import { useState } from 'react';
import { Message } from './ChatWindow';
import Lightbox, { GenericSlide } from 'yet-another-react-lightbox';
import 'yet-another-react-lightbox/styles.css';
type Document = {
url: string;
title: string;
snippet: string;
source: string;
type: string;
iframe_src: string;
};
declare module 'yet-another-react-lightbox' {
export interface PDFSlide extends GenericSlide {
type: 'pdf';
url: string;
iframe_src: string;
}
interface SlideTypes {
'pdf': PDFSlide;
}
}
const LegalSearch = ({
query,
chatHistory,
}: {
query: string;
chatHistory: Message[];
}) => {
const [documents, setDocuments] = useState<Document[] | null>(null);
const [loading, setLoading] = useState(false);
const [open, setOpen] = useState(false);
const [currentDoc, setCurrentDoc] = useState<Document | null>(null);
const openDocument = (doc: Document) => {
setCurrentDoc(doc);
setOpen(true);
};
return (
<>
{!loading && documents === null && (
<button
onClick={async () => {
setLoading(true);
const chatModelProvider = localStorage.getItem('chatModelProvider');
const chatModel = localStorage.getItem('chatModel');
const customOpenAIBaseURL = localStorage.getItem('openAIBaseURL');
const customOpenAIKey = localStorage.getItem('openAIApiKey');
const res = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/legal`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: query,
chatHistory: chatHistory,
chatModel: {
provider: chatModelProvider,
model: chatModel,
...(chatModelProvider === 'custom_openai' && {
customOpenAIBaseURL: customOpenAIBaseURL,
customOpenAIKey: customOpenAIKey,
}),
},
}),
},
);
const data = await res.json();
setDocuments(data.documents ?? []);
setLoading(false);
}}
className="border border-dashed border-light-200 dark:border-dark-200 hover:bg-light-200 dark:hover:bg-dark-200 active:scale-95 duration-200 transition px-4 py-2 flex flex-row items-center justify-between rounded-lg dark:text-white text-sm w-full"
>
<div className="flex flex-row items-center space-x-2">
<BookCopy size={17} />
<p>Rechercher des textes légaux</p>
</div>
<PlusIcon className="text-[#24A0ED]" size={17} />
</button>
)}
{loading && (
<div className="flex flex-col space-y-2">
{[...Array(3)].map((_, i) => (
<div
key={i}
className="bg-light-secondary dark:bg-dark-secondary h-24 w-full rounded-lg animate-pulse"
/>
))}
</div>
)}
{documents !== null && documents.length > 0 && (
<>
<div className="flex flex-col space-y-2">
{documents.length > 4
? documents.slice(0, 3).map((doc, i) => (
<div
key={i}
onClick={() => openDocument(doc)}
className="bg-light-100 dark:bg-dark-100 p-3 rounded-lg hover:bg-light-200 dark:hover:bg-dark-200 transition duration-200 cursor-pointer"
>
<h4 className="text-sm font-medium text-black dark:text-white line-clamp-2">
{doc.title}
</h4>
<p className="text-xs text-black/50 dark:text-white/50 mt-1 line-clamp-2">
{doc.snippet}
</p>
<div className="flex items-center space-x-2 mt-2">
<span className="text-xs text-black/30 dark:text-white/30">
{doc.source}
</span>
<span className="text-xs bg-light-secondary dark:bg-dark-secondary px-1.5 py-0.5 rounded text-black/50 dark:text-white/50">
{doc.type}
</span>
</div>
</div>
))
: documents.map((doc, i) => (
<div
key={i}
onClick={() => openDocument(doc)}
className="bg-light-100 dark:bg-dark-100 p-3 rounded-lg hover:bg-light-200 dark:hover:bg-dark-200 transition duration-200 cursor-pointer"
>
<h4 className="text-sm font-medium text-black dark:text-white line-clamp-2">
{doc.title}
</h4>
<p className="text-xs text-black/50 dark:text-white/50 mt-1 line-clamp-2">
{doc.snippet}
</p>
<div className="flex items-center space-x-2 mt-2">
<span className="text-xs text-black/30 dark:text-white/30">
{doc.source}
</span>
<span className="text-xs bg-light-secondary dark:bg-dark-secondary px-1.5 py-0.5 rounded text-black/50 dark:text-white/50">
{doc.type}
</span>
</div>
</div>
))}
{documents.length > 4 && (
<button
onClick={() => openDocument(documents[3])}
className="bg-light-100 hover:bg-light-200 dark:bg-dark-100 dark:hover:bg-dark-200 transition duration-200 p-3 rounded-lg text-black/70 dark:text-white/70 text-sm"
>
Voir {documents.length - 3} documents supplémentaires
</button>
)}
</div>
<Lightbox
open={open}
close={() => setOpen(false)}
render={{
slide: ({ slide }) =>
slide.type === 'pdf' ? (
<div className="h-full w-full flex flex-col items-center justify-center">
<div className="text-center mb-4 text-white">
<p>Le document ne peut pas être affiché directement.</p>
<a
href={slide.url}
target="_blank"
rel="noopener noreferrer"
className="text-blue-400 hover:text-blue-300 underline mt-2 inline-block"
>
Ouvrir le document dans un nouvel onglet
</a>
</div>
<div className="bg-white/10 p-4 rounded-lg max-w-2xl">
<h3 className="text-white/90 font-medium mb-2">{currentDoc?.title}</h3>
<p className="text-white/70 text-sm">{currentDoc?.snippet}</p>
</div>
</div>
) : null,
}}
slides={[
{
type: 'pdf',
url: currentDoc?.url || '',
iframe_src: currentDoc?.url || '',
}
]}
/>
</>
)}
</>
);
};
export default LegalSearch;

View file

@ -16,7 +16,7 @@ import Markdown from 'markdown-to-jsx';
import Copy from './MessageActions/Copy';
import Rewrite from './MessageActions/Rewrite';
import MessageSources from './MessageSources';
import SearchImages from './SearchImages';
import LegalSearch from './LegalSearch';
import SearchVideos from './SearchVideos';
import { useSpeech } from 'react-text-to-speech';
@ -53,8 +53,12 @@ const MessageBox = ({
return setParsedMessage(
message.content.replace(
regex,
(_, number) =>
`<a href="${message.sources?.[number - 1]?.metadata?.url}" target="_blank" className="bg-light-secondary dark:bg-dark-secondary px-1 rounded ml-1 no-underline text-xs text-black/70 dark:text-white/70 relative">${number}</a>`,
(_, number) => {
const url = message.sources?.[number - 1]?.metadata?.url || '';
// Extraire le nom de domaine sans l'extension
const sourceName = url.replace(/^(?:https?:\/\/)?(?:www\.)?([^./]+).*$/, '$1');
return `<a href="${url}" target="_blank" class="ml-2 px-3 py-1 text-xs bg-blue-500 hover:bg-blue-600 text-white rounded-md transition-colors duration-200 no-underline inline-flex items-center">${sourceName}</a>`;
}
),
);
}
@ -63,15 +67,18 @@ const MessageBox = ({
setParsedMessage(message.content);
}, [message.content, message.sources, message.role]);
useEffect(() => {
}, [message.sources]);
const { speechStatus, start, stop } = useSpeech({ text: speechMessage });
return (
<div>
{message.role === 'user' && (
<div className={cn('w-full', messageIndex === 0 ? 'pt-16' : 'pt-8')}>
<h2 className="text-black dark:text-white font-medium text-3xl lg:w-9/12">
<h3 className="text-black dark:text-white font-medium text-3xl lg:w-9/12">
{message.content}
</h2>
</h3>
</div>
)}
@ -81,6 +88,24 @@ const MessageBox = ({
ref={dividerRef}
className="flex flex-col space-y-6 w-full lg:w-9/12"
>
{message.sources && message.sources[0]?.metadata?.illustrationImage && (
<div className="flex flex-col space-y-2 mb-6">
<div className="w-full aspect-[21/9] relative overflow-hidden rounded-xl shadow-lg">
<img
src={message.sources[0].metadata.illustrationImage}
alt="Illustration"
className="w-full h-full object-cover hover:scale-105 transition-transform duration-300"
onError={(e) => {
console.error("Erreur de chargement de l'image:", e);
(e.target as HTMLImageElement).style.display = 'none';
}}
/>
</div>
<p className="text-sm text-gray-500 dark:text-gray-400 italic mt-2">
{message.sources[0].metadata.title || 'Illustration du sujet'}
</p>
</div>
)}
{message.sources && message.sources.length > 0 && (
<div className="flex flex-col space-y-2">
<div className="flex flex-row items-center space-x-2">
@ -102,7 +127,7 @@ const MessageBox = ({
size={20}
/>
<h3 className="text-black dark:text-white font-medium text-xl">
Answer
Question
</h3>
</div>
<Markdown
@ -110,6 +135,13 @@ const MessageBox = ({
'prose prose-h1:mb-3 prose-h2:mb-2 prose-h2:mt-6 prose-h2:font-[800] prose-h3:mt-4 prose-h3:mb-1.5 prose-h3:font-[600] dark:prose-invert prose-p:leading-relaxed prose-pre:p-0 font-[400]',
'max-w-none break-words text-black dark:text-white',
)}
options={{
overrides: {
p: ({ children }) => {
return <p>{children}</p>;
},
},
}}
>
{parsedMessage}
</Markdown>
@ -152,7 +184,7 @@ const MessageBox = ({
<div className="flex flex-col space-y-3 text-black dark:text-white">
<div className="flex flex-row items-center space-x-2 mt-4">
<Layers3 />
<h3 className="text-xl font-medium">Related</h3>
<h3 className="text-xl font-medium">Suggestions</h3>
</div>
<div className="flex flex-col space-y-3">
{message.suggestions.map((suggestion, i) => (
@ -184,7 +216,7 @@ const MessageBox = ({
</div>
</div>
<div className="lg:sticky lg:top-20 flex flex-col items-center space-y-3 w-full lg:w-3/12 z-30 h-full pb-4">
<SearchImages
<LegalSearch
query={history[messageIndex - 1].content}
chatHistory={history.slice(0, messageIndex - 1)}
/>

View file

@ -4,7 +4,7 @@ import {
Globe,
Pencil,
ScanEye,
SwatchBook,
Eye,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import {
@ -19,52 +19,28 @@ import { Fragment } from 'react';
const focusModes = [
{
key: 'webSearch',
title: 'All',
description: 'Searches across all of the internet',
title: 'Recherche internet',
description: 'Recherche sur internet directement',
icon: <Globe size={20} />,
},
{
key: 'academicSearch',
title: 'Academic',
description: 'Search in published academic papers',
icon: <SwatchBook size={20} />,
title: 'Experts',
description: 'Recherche un expert pour vous acccompagner',
icon: <Eye size={20} />,
},
{
key: 'writingAssistant',
title: 'Writing',
title: 'Document',
description: 'Chat without searching the web',
icon: <Pencil size={16} />,
},
{
key: 'wolframAlphaSearch',
title: 'Wolfram Alpha',
description: 'Computational knowledge engine',
title: 'Business Plan',
description: 'Réaliser votre Business Plan',
icon: <BadgePercent size={20} />,
},
{
key: 'youtubeSearch',
title: 'Youtube',
description: 'Search and watch videos',
icon: (
<SiYoutube
className="h-5 w-auto mr-0.5"
onPointerEnterCapture={undefined}
onPointerLeaveCapture={undefined}
/>
),
},
{
key: 'redditSearch',
title: 'Reddit',
description: 'Search for discussions and opinions',
icon: (
<SiReddit
className="h-5 w-auto mr-0.5"
onPointerEnterCapture={undefined}
onPointerLeaveCapture={undefined}
/>
),
},
];
const Focus = ({

View file

@ -23,54 +23,60 @@ const SearchImages = ({
const [open, setOpen] = useState(false);
const [slides, setSlides] = useState<any[]>([]);
const handleSearch = async () => {
setLoading(true);
try {
console.log("🖼️ Démarrage de la recherche d'images pour:", query);
const chatModelProvider = localStorage.getItem('chatModelProvider');
const chatModel = localStorage.getItem('chatModel');
console.log("🖼️ Modèle configuré:", chatModelProvider, chatModel);
const response = await fetch('/api/images', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: query,
chatHistory: chatHistory,
chatModel: {
provider: chatModelProvider,
model: chatModel,
},
}),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log('🖼️ Résultats de la recherche:', data);
if (data.images && data.images.length > 0) {
setImages(data.images);
setSlides(
data.images.map((image: Image) => ({
src: image.img_src,
}))
);
console.log('🖼️ Images et slides mis à jour:', data.images.length);
} else {
console.log('🖼️ Aucune image trouvée');
}
} catch (error) {
console.error('🖼️ Erreur lors de la recherche:', error);
} finally {
setLoading(false);
}
};
return (
<>
{!loading && images === null && (
<button
onClick={async () => {
setLoading(true);
const chatModelProvider = localStorage.getItem('chatModelProvider');
const chatModel = localStorage.getItem('chatModel');
const customOpenAIBaseURL = localStorage.getItem('openAIBaseURL');
const customOpenAIKey = localStorage.getItem('openAIApiKey');
const res = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/images`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: query,
chatHistory: chatHistory,
chatModel: {
provider: chatModelProvider,
model: chatModel,
...(chatModelProvider === 'custom_openai' && {
customOpenAIBaseURL: customOpenAIBaseURL,
customOpenAIKey: customOpenAIKey,
}),
},
}),
},
);
const data = await res.json();
const images = data.images ?? [];
setImages(images);
setSlides(
images.map((image: Image) => {
return {
src: image.img_src,
};
}),
);
setLoading(false);
}}
onClick={handleSearch}
className="border border-dashed border-light-200 dark:border-dark-200 hover:bg-light-200 dark:hover:bg-dark-200 active:scale-95 duration-200 transition px-4 py-2 flex flex-row items-center justify-between rounded-lg dark:text-white text-sm w-full"
>
<div className="flex flex-row items-center space-x-2">

View file

@ -1,7 +1,7 @@
'use client';
import { cn } from '@/lib/utils';
import { BookOpenText, Home, Search, SquarePen, Settings } from 'lucide-react';
import { BookOpenText, Home, Search, SquarePen, Settings, MessageSquare } from 'lucide-react';
import Link from 'next/link';
import { useSelectedLayoutSegments } from 'next/navigation';
import React, { useState, type ReactNode } from 'react';
@ -38,6 +38,12 @@ const Sidebar = ({ children }: { children: React.ReactNode }) => {
active: segments.includes('library'),
label: 'Library',
},
{
icon: MessageSquare,
href: '/chatroom',
active: segments.includes('chatroom'),
label: 'Messages',
},
];
return (

View file

@ -0,0 +1,52 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

121
ui/components/ui/dialog.tsx Normal file
View file

@ -0,0 +1,121 @@
"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<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/50 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-light-200 dark:border-dark-200 bg-light-primary dark:bg-dark-primary p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-white transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-light-200 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-light-100 data-[state=open]:text-gray-500 dark:ring-offset-dark-200 dark:focus:ring-dark-200 dark:data-[state=open]:bg-dark-100 dark:data-[state=open]:text-gray-400">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-gray-500 dark:text-gray-400", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

View file

@ -0,0 +1,24 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }