feat: ask ai

This commit is contained in:
dextmorgn
2025-02-14 15:33:28 +01:00
parent f87fa514bf
commit ec76fb63a2
23 changed files with 219 additions and 197 deletions

View File

@@ -0,0 +1,12 @@
import { Button } from "@/components/ui/button"
import { GemIcon } from "lucide-react"
import { useChatContext } from "./contexts/chatbot-context"
const Assistant = () => {
const { handleOpenChat } = useChatContext()
return (
<Button onClick={() => handleOpenChat(null)} variant="ghost" size={"icon"} className="h-full w-12 rounded-none"><GemIcon className="h-6 w-6 text-primary" /></Button>
)
}
export default Assistant

View File

@@ -6,147 +6,151 @@ import { useChat } from "@ai-sdk/react"
import { Bot, User } from "lucide-react"
import { cn } from "@/lib/utils"
import ReactMarkdown from "react-markdown"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Card, CardContent } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import { ScrollArea } from "../ui/scroll-area"
interface ChatContextType {
open: boolean
handleOpenChat: (content: any) => void
open: boolean
handleOpenChat: (content: any) => void
}
const ChatContext = createContext<ChatContextType | undefined>(undefined)
interface ChatProviderProps {
children: ReactNode
children: ReactNode
}
export const ChatProvider: React.FC<ChatProviderProps> = ({ children }) => {
const [open, setOpen] = useState(false)
const [context, setContext] = useState<any>(null)
const { messages, input, handleInputChange, setInput, handleSubmit, error, isLoading } = useChat()
const [open, setOpen] = useState(false)
const [context, setContext] = useState<any>(null)
const { messages, input, handleInputChange, setInput, handleSubmit, error, isLoading } = useChat()
const handleOpenChat = (content: any) => {
setContext(content)
setInput(`What pattern can you extract from this person ?\n ${JSON.stringify(content)}`)
setOpen(true)
}
const handleOpenChat = (content: any) => {
setContext(content)
if (content)
setInput(`What pattern can you extract from this person ?\n ${JSON.stringify(content)}`)
setOpen(true)
}
return (
<ChatContext.Provider value={{ open, handleOpenChat }}>
{children}
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className={cn("w-full p-4", messages?.length > 0 ? "sm:max-w-[730px]" : "sm:max-w-[480px]")}>
<DialogHeader>
<DialogTitle>Ask AI</DialogTitle>
<DialogDescription>Ask AI chat bot to help you understand patterns.</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-3 overflow-y-auto w-full max-h-[60vh]">
{messages.map((m) => (
<div key={m.id} className="flex grow">
<div
className={cn(
"flex gap-1 items-start w-full",
m.role === "user" ? "flex-row-reverse justify-end" : "flex-row justify-start",
)}
>
<Avatar className={cn("h-8 w-8")}>
<AvatarFallback>
{m.role === "user" ? (
<User className="h-4 w-4" />
) : isLoading ? (
<LoadingDots />
) : (
<Bot className="h-4 w-4" />
)}
</AvatarFallback>
</Avatar>
<div className={cn("flex flex-col gap-1 w-full", m.role === "user" ? "items-end" : "items-start")}>
<span className={cn("text-sm font-bold", m.role === "user" ? "text-right" : "text-left")}>
{isLoading && m.role !== "user" ? <LoadingDots /> : m.role === "user" ? "User" : "Chatbot"}
</span>
<Card className="max-w-[80%]">
<CardContent className="p-3">
<ReactMarkdown>{m.content}</ReactMarkdown>
</CardContent>
</Card>
</div>
</div>
</div>
))}
{error && (
<div className="flex grow">
<div className="flex gap-1 items-start justify-start w-full">
<Avatar className="h-8 w-8 bg-red-500">
<AvatarFallback>
<Bot className="h-4 w-4" />
</AvatarFallback>
</Avatar>
<div className="flex flex-col gap-1 items-start w-full">
<span className="text-sm font-bold text-red-500">Error</span>
<Card className="max-w-[80%]">
<CardContent className="p-3 text-red-500">
Oops, an error occurred. Make sure you provided a valid Mistral API key.
</CardContent>
</Card>
</div>
</div>
</div>
)}
</div>
<form onSubmit={handleSubmit} className="mt-4">
<Input
value={input}
onChange={handleInputChange}
placeholder="'What pattern can you extract from those relations ?'"
className="w-full"
/>
<Button type="submit" className="mt-2">
Send
</Button>
</form>
</DialogContent>
</Dialog>
</ChatContext.Provider>
)
return (
<ChatContext.Provider value={{ open, handleOpenChat }}>
{children}
<Sheet open={open} onOpenChange={setOpen}>
<SheetContent className={cn("w-full p-4 flex divide-y flex-col justify-between", messages?.length > 0 ? "sm:max-w-[730px]" : "sm:max-w-[480px]")}>
<SheetHeader>
<SheetTitle>Ask AI</SheetTitle>
<SheetDescription>Ask AI chat bot to help you understand patterns.</SheetDescription>
</SheetHeader>
<ScrollArea className="flex flex-col gap-3 overflow-y-auto w-full grow">
{messages.map((m) => (
<div key={m.id} className="flex grow mb-2">
<div
className={cn(
"flex gap-1 items-start w-full",
m.role === "user" ? "flex-row-reverse justify-end" : "flex-row justify-start",
)}
>
<Avatar className={cn("h-8 w-8")}>
<AvatarFallback>
{m.role === "user" ? (
<User className="h-4 w-4" />
) : isLoading ? (
<LoadingDots />
) : (
<Bot className="h-4 w-4" />
)}
</AvatarFallback>
</Avatar>
<div className={cn("flex flex-col gap-1 w-full", m.role === "user" ? "items-end" : "items-start")}>
<span className={cn("text-sm font-bold", m.role === "user" ? "text-right" : "text-left")}>
{isLoading && m.role !== "user" ? <LoadingDots /> : m.role === "user" ? "User" : "Chatbot"}
</span>
<Card className="max-w-[80%]">
<CardContent className="p-3">
<ReactMarkdown>{m.content}</ReactMarkdown>
</CardContent>
</Card>
</div>
</div>
</div>
))}
{error && (
<div className="flex">
<div className="flex gap-1 items-start justify-start w-full">
<Avatar className="h-8 w-8 bg-red-500">
<AvatarFallback>
<Bot className="h-4 w-4" />
</AvatarFallback>
</Avatar>
<div className="flex flex-col gap-1 items-start w-full">
<span className="text-sm font-bold text-red-500">Error</span>
<Card className="max-w-[80%]">
<CardContent className="p-3 text-red-500">
Oops, an error occurred. Make sure you provided a valid Mistral API key.
</CardContent>
</Card>
</div>
</div>
</div>
)}
</ScrollArea>
<form onSubmit={handleSubmit} className=" flex items-center gap-2">
<Input
value={input}
onChange={handleInputChange}
placeholder="'What pattern can you extract from those relations ?'"
className="grow"
/>
<div>
<Button type="submit">
Send
</Button>
</div>
</form>
</SheetContent>
</Sheet>
</ChatContext.Provider>
)
}
export const useChatContext = (): ChatContextType => {
const context = useContext(ChatContext)
if (!context) {
throw new Error("useChatContext must be used within a ChatProvider")
}
return context
const context = useContext(ChatContext)
if (!context) {
throw new Error("useChatContext must be used within a ChatProvider")
}
return context
}
interface LoadingDotsProps {
speed?: number
text?: string
speed?: number
text?: string
}
const LoadingDots: React.FC<LoadingDotsProps> = ({ speed = 200, text = "Thinking" }) => {
const [dots, setDots] = useState("")
const [dots, setDots] = useState("")
useEffect(() => {
const interval = setInterval(() => {
setDots((prevDots) => {
if (prevDots.length >= 3) {
return ""
}
return prevDots + "."
})
}, speed)
useEffect(() => {
const interval = setInterval(() => {
setDots((prevDots) => {
if (prevDots.length >= 3) {
return ""
}
return prevDots + "."
})
}, speed)
return () => clearInterval(interval)
}, [speed])
return () => clearInterval(interval)
}, [speed])
return (
<div className="flex items-center">
<span>{text}</span>
<span className="w-8 text-left">{dots}</span>
</div>
)
return (
<div className="flex items-center">
<span>{text}</span>
<span className="w-8 text-left">{dots}</span>
</div>
)
}

View File

@@ -95,7 +95,7 @@ export const InvestigationProvider: React.FC<InvestigationProviderProps> = ({ ch
return (
<InvestigationContext.Provider value={{ filters, setFilters, settings, setSettings, setOpenSettingsModal, investigation, isLoadingInvestigation, handleOpenIndividualModal, handleDeleteInvestigation, currentNode, setCurrentNode, panelOpen, setPanelOpen }}>
{children}
<Dialog open={openSettingsModal}>
<Dialog open={openSettingsModal} onOpenChange={setOpenSettingsModal}>
<DialogContent>
<DialogTitle>Settings</DialogTitle>
<DialogDescription>
@@ -119,8 +119,8 @@ export const InvestigationProvider: React.FC<InvestigationProviderProps> = ({ ch
</div>
</div>
<div className="flex items-center gap-2 justify-end">
<DialogClose asChild onClick={() => setOpenSettingsModal(false)}>
<Button variant="outline">
<DialogClose asChild>
<Button type="button" variant="outline">
Cancel
</Button>
</DialogClose>

View File

@@ -189,7 +189,7 @@ export const NodeProvider: React.FC<NodeProviderProps> = (props: any) => {
</Alert>}
<div className="flex items-center gap-2 justify-end mt-4">
<DialogClose asChild>
<Button variant="outline">
<Button type="button" variant="outline">
Cancel
</Button>
</DialogClose>

View File

@@ -4,13 +4,15 @@ import { createContext, useContext, type ReactNode, useState } from "react"
import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet"
import { Alert, AlertDescription } from "@/components/ui/alert"
import { InfoIcon } from "lucide-react"
import { investigateValue } from "@/lib/actions/search"
@@ -66,12 +68,12 @@ export const SearchProvider: React.FC<SearchProviderProps> = ({ children }) => {
return (
<SearchContext.Provider value={{ openSearchModal, handleOpenSearchModal }}>
{children}
<Dialog open={openSearchModal} onOpenChange={handleCloseModal}>
<DialogContent className={results ? "sm:max-w-[950px]" : "sm:max-w-[450px]"}>
<DialogHeader>
<DialogTitle>New search</DialogTitle>
<DialogDescription>Make a new keyword associated research.</DialogDescription>
</DialogHeader>
<Sheet open={openSearchModal} onOpenChange={handleCloseModal}>
<SheetContent className={results ? "sm:max-w-[950px]" : "sm:max-w-[450px]"}>
<SheetHeader>
<SheetTitle>New search</SheetTitle>
<SheetDescription>Make a new keyword associated research.</SheetDescription>
</SheetHeader>
<form onSubmit={onSubmitNewSearch}>
<div className="flex flex-col gap-3">
<Card className="p-4">
@@ -104,20 +106,22 @@ export const SearchProvider: React.FC<SearchProviderProps> = ({ children }) => {
)
)}
</div>
<DialogFooter className="mt-4">
<Button variant="outline" onClick={handleCloseModal}>
Close
</Button>
<SheetFooter className="mt-4">
<SheetClose asChild>
<Button type="button" variant="outline">
Close
</Button>
</SheetClose>
{!results && (
<Button disabled={isLoading} type="submit">
{isLoading ? "Saving..." : "Save"}
</Button>
)}
</DialogFooter>
</SheetFooter>
</form>
</DialogContent>
</Dialog>
</SearchContext.Provider>
</SheetContent>
</Sheet>
</SearchContext.Provider >
)
}

View File

@@ -140,18 +140,18 @@ const createStore = (initialNodes: AppNode[] = [], initialEdges: Edge[] = []) =>
},
onNodeClick: (_event: any, node: { id: string; }) => {
set({ currentNode: node.id });
get().resetNodeStyles()
get().resetNodeStyles();
},
onPaneClick: (_envent: any) => {
set({ currentNode: null });
get().resetNodeStyles()
get().resetNodeStyles();
},
onLayout: (direction = 'TB', fitView: () => void) => {
const { nodes, edges } = getLayoutedElements(get().nodes, get().edges, { direction });
set({ nodes });
set({ edges })
fitView()
set({ edges });
fitView();
},
highlightPath: (selectedNode: Node | null) => {
if (!selectedNode) {

View File

@@ -1,10 +1,11 @@
"use client"
import { useState, useCallback } from "react"
import { Copy } from "lucide-react"
import { CopyIcon } from "lucide-react"
import { useTimeout } from "usehooks-ts"
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"
import { Button } from "./ui/button"
import { cn } from "@/lib/utils"
interface CopyButtonProps {
content: string
@@ -34,8 +35,8 @@ export function CopyButton({ content, className, delay = 2000 }: CopyButtonProps
return (
<Tooltip open={isCopied}>
<TooltipTrigger asChild>
<Button className={className} size={"icon"} variant="ghost" onClick={handleCopy} aria-label="Copy content">
<Copy className="h-3 w-3 opacity-50" />
<Button className={cn("h-7 w-7", className)} size={"icon"} variant="ghost" onClick={handleCopy} aria-label="Copy content">
<CopyIcon className="!h-3.5 !w-3.5 opacity-50" />
</Button>
</TooltipTrigger>
<TooltipContent>

View File

@@ -3,32 +3,29 @@ import React from 'react'
import { Investigation } from '@/types/investigation';
import Link from 'next/link';
import Image from 'next/image';
import { Card } from '@/components/ui/card';
const investigation = ({ investigation }: { investigation: Investigation }) => {
return (
<Link href={`/investigations/${investigation.id}`} >
<div className="w-full h-full">
<Card className='h-full'>
<div className="group relative h-full overflow-hidden rounded-lg">
<div className="aspect-[4/3] overflow-hidden">
<Image
src={"https://c6p3q0oludlschf8w.lite.vusercontent.net/placeholder.svg"}
alt={investigation.title}
width={400}
height={300}
className="h-full w-full object-cover transition-transform group-hover:scale-115 dark:invert"
/>
<div className="group relative h-full overflow-hidden">
<div className="aspect-video overflow-hidden border">
<Image
src={"https://c6p3q0oludlschf8w.lite.vusercontent.net/placeholder.svg"}
alt={investigation.title}
width={400}
height={300}
className="h-full w-full object-cover transition-transform group-hover:scale-115 dark:invert"
/>
</div>
<div className="p-4 h-full">
<div>
{investigation.title}
</div>
<div className="p-4 h-full">
<div>
{investigation.title}
</div>
<div>
{investigation.description || <span className='italic'>No description provided.</span>}
</div>
<div className='opacity-60'>
{investigation.description || <span className='italic'>No description provided.</span>}
</div>
</div>
</Card>
</div>
</div>
</Link>
)

View File

@@ -82,7 +82,7 @@ const SearchModal = ({ investigation_id }: { investigation_id: string }) => {
<Button variant="outline" size="icon" onClick={() => setOpen(true)}>
<Search className="h-4 w-4" />
</Button>
<DialogContent className="sm:max-w-[425px]">
<DialogContent className="sm:max-w-[425px] min-h-none">
<DialogHeader>
<DialogTitle>Search</DialogTitle>
<DialogDescription>
@@ -99,13 +99,13 @@ const SearchModal = ({ investigation_id }: { investigation_id: string }) => {
{error && <p className="text-red-500">An error occurred.</p>}
{isLoading && <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div>}
{results?.length === 0 && <p>No results found for "{search}".</p>}
<ScrollArea className="h-[60vh] w-full rounded-md border p-4">
{results && results?.length !== 0 && <ScrollArea className="h-[60vh] w-full rounded-md border p-4">
<ul className='space-y-2'>
{!error && !isLoading && Array.isArray(results) && results?.map((item) => (
<SearchItem key={item.id} item={item} />
))}
</ul>
</ScrollArea>
</ScrollArea>}
</div>
</div>
</DialogContent>

View File

@@ -18,10 +18,10 @@ export default function CaseSelector() {
router.push(`/investigations/${value}`);
};
return (
<div className="ml-2 flex items-center">
<div className="flex items-center">
{isLoading || isLoadingInvestigation ? <Skeleton className="h-8 w-40" /> :
<Select onValueChange={handleSelectionChange} defaultValue={investigation?.id}>
<SelectTrigger className="min-w-none w-full text-ellipsis truncate">
<SelectTrigger className="min-w-none w-full shadow-none text-ellipsis truncate gap-1">
<SelectValue defaultValue={investigation?.title || ""} placeholder="Select a fruit" />
</SelectTrigger>
<SelectContent>

View File

@@ -47,7 +47,7 @@ function FloatingEdge(props: any) {
pointerEvents: 'all',
opacity: style?.opacity || 1
}}
className="nodrag nopan !text-[.6rem] !px-1 !py-0">{label} {confidence_level &&
className="nodrag nopan bg-primary/80 backdrop-blur leading-3.5 border border-primary !text-[.6rem] !px-1 !py-0">{label} {confidence_level &&
<>{confidence_level}%</>}
</Badge>}
</EdgeLabelRenderer></>

View File

@@ -27,7 +27,6 @@ import { Tooltip, TooltipContent } from '@/components/ui/tooltip';
import { Button } from '@/components/ui/button';
import { TooltipTrigger } from '@radix-ui/react-tooltip';
import { Card } from '@/components/ui/card';
import { getInvestigationData } from '@/lib/actions/investigations';
import { cn } from '@/lib/utils';
import { useInvestigationContext } from '@/components/contexts/investigation-provider';
import { useFlowStore } from '../contexts/use-flow-store';
@@ -141,7 +140,7 @@ const LayoutFlow = ({ theme }: { theme: ColorMode }) => {
<NewActions addNodes={addNodes} />
</div>
{currentNode && (
<Card>
<Card className='p-3'>
<>
{getNode(currentNode)?.data?.label}
</>

View File

@@ -100,6 +100,7 @@ const IndividualModal = () => {
<Button
variant="ghost"
size="icon"
type="button"
onClick={() => setEditMode(!editMode)}
aria-label={editMode ? "Cancel edit" : "Edit profile"}
>
@@ -113,8 +114,8 @@ const IndividualModal = () => {
<AvatarFallback>{individual?.full_name?.[0] || "?"}</AvatarFallback>
</Avatar>
</div>
<div className="flex-grow">
<Tabs defaultValue="overview">
<div className="flex-grow w-full">
<Tabs defaultValue="overview" className="w-full">
<TabsList className="overflow-x-auto">
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="social_account">

View File

@@ -12,6 +12,7 @@ import { useInvestigationContext } from '@/components/contexts/investigation-pro
import { NavUser } from '@/components/user';
import { Button } from '../ui/button';
import { ScrollArea } from '../ui/scroll-area';
import Assistant from '../assistant';
const InvestigationLayout = ({
children,
@@ -24,8 +25,7 @@ const InvestigationLayout = ({
investigation_id: string
user: any
}) => {
const { handleOpenChat } = useChatContext()
const { panelOpen, setPanelOpen, investigation } = useInvestigationContext()
const { panelOpen, setPanelOpen } = useInvestigationContext()
return (
<PanelGroup autoSaveId="conditional" className='h-screen w-screen flex' direction="horizontal">
{panelOpen && <Panel id="left" order={1} className='h-screen' defaultSize={20} minSize={15}>
@@ -33,7 +33,6 @@ const InvestigationLayout = ({
<div className='w-full rounded-none shadow-none h-12 border-b flex items-center gap-1 flex-row justify-between p-2'>
<Logo />
<div className='flex gap-1'>
<Button variant="outline" size="icon" onClick={() => handleOpenChat(investigation)} color="gray"><BotMessageSquareIcon className="h-4" /></Button>
<SearchModal investigation_id={investigation_id} />
<NewCase>
<Button variant="outline" size="icon">
@@ -55,14 +54,19 @@ const InvestigationLayout = ({
<PanelResizeHandle />
<Panel id="right" order={2} defaultSize={80} minSize={50} className='grow flex flex-col'>
<div>
<div className='w-full rounded-none shadow-none h-12 justify-between border-b flex flex-row items-center p-2'>
<div className='flex gap-1'>
<Button onClick={() => setPanelOpen(!panelOpen)} size="icon" variant="outline">
<PanelRightIcon className='h-4 w-4' />
</Button>
<CaseSelector />
<div className='w-full rounded-none shadow-none h-12 justify-between border-b flex flex-row items-center'>
<div className='grow flex items-center justify-between p-2'>
<div className='flex gap-1 p-2'>
<Button onClick={() => setPanelOpen(!panelOpen)} size="icon" variant="outline">
<PanelRightIcon className='h-4 w-4' />
</Button>
<CaseSelector />
</div>
<MoreMenu />
</div>
<div className='border-l h-full flex items-center'>
<Assistant />
</div>
<MoreMenu />
</div>
{children}
</div>

View File

@@ -44,7 +44,7 @@ function EmailNode({ data }: any) {
<Badge variant="secondary" className="h-6 w-6 p-0 rounded-full">
<LocateIcon className="h-4 w-4" />
</Badge>
<div className="flex items-center gap-2">
<div className="flex items-center gap-1">
<span className="text-sm">{data.label}</span>
{settings.showCopyIcon && <CopyButton className="rounded-full h-7 w-7 text-xs" content={data.label} />}
</div>

View File

@@ -89,7 +89,7 @@ function Custom(props: any) {
<AvatarFallback>{loading ? "..." : data.full_name[0]}</AvatarFallback>
</Avatar>
{settings.showNodeLabel && showContent && (
<div className="flex items-center gap-2">
<div className="flex items-center gap-1">
<span className="text-sm">{data.full_name}</span>
{settings.showCopyIcon && <CopyButton className="rounded-full" content={data.full_name} />}
</div>

View File

@@ -44,7 +44,7 @@ function IpNode({ data }: any) {
<Badge variant="secondary" className="h-6 w-6 p-0 rounded-full">
<LocateIcon className="h-4 w-4" />
</Badge>
<div className="flex items-center gap-2">
<div className="flex items-center gap-1">
<span className="text-sm">{data.label}</span>
{settings.showCopyIcon && <CopyButton className="rounded-full h-7 w-7 text-xs" content={data.label} />}
</div>

View File

@@ -44,7 +44,7 @@ function PhoneNode({ data }: any) {
<Badge variant="secondary" className="h-6 w-6 p-0 rounded-full">
<PhoneIcon className="h-4 w-4" />
</Badge>
<div className="flex items-center gap-2">
<div className="flex items-center gap-1">
<span className="text-sm">{data.label}</span>
{settings.showCopyIcon && <CopyButton className="rounded-full h-7 w-7 text-xs" content={data.label} />}
</div>

View File

@@ -44,7 +44,7 @@ function AddressNode({ data }: any) {
<Badge variant="secondary" className="h-6 w-6 p-0 rounded-full">
<MapPinIcon className="h-4 w-4" />
</Badge>
<div className="flex items-center gap-2">
<div className="flex items-center gap-1">
<span className="text-sm">{data.label}</span>
{settings.showCopyIcon && <CopyButton className="rounded-full h-7 w-7 text-xs" content={data.label} />}
</div>

View File

@@ -49,7 +49,7 @@ function SocialNode({ data }: any) {
{/* @ts-ignore */}
{platformsIcons?.[data?.platform]?.icon}
</Badge>
<div className="flex items-center gap-2">
<div className="flex items-center gap-1">
<span className="text-sm">{data.username || data.profile_url}</span>
{settings.showCopyIcon && <CopyButton className="rounded-full h-7 w-7 text-xs" content={data.username || data.profile_url} />}
</div>

View File

@@ -10,13 +10,13 @@ const buttonVariants = cva(
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-sm hover:bg-primary/90",
"bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90",
"bg-destructive text-destructive-foregroundhover:bg-destructive/90",
outline:
"border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground",
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
"bg-secondary text-secondary-foregroundhover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},

View File

@@ -58,7 +58,7 @@ function SheetContent({
<SheetPrimitive.Content
data-slot="sheet-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-100 data-[state=open]:duration-300",
side === "right" &&
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
side === "left" &&

View File

@@ -63,7 +63,7 @@
--accent-foreground: hsl(0 0% 98%);
--destructive: hsl(0 62.8% 30.6%);
--destructive-foreground: hsl(0 0% 98%);
--border: hsl(0 0% 14.9%);
--border: hsl(0, 0%, 23%);
--input: hsl(0 0% 14.9%);
--ring: hsl(0 0% 83.1%);
--chart-1: hsl(220 70% 50%);