mirror of
https://github.com/khoj-ai/khoj.git
synced 2025-02-17 08:04:21 +00:00
Format next.js web app with prettier
This commit is contained in:
parent
41bdd6d6d9
commit
842036688d
64 changed files with 5806 additions and 5087 deletions
|
@ -8,18 +8,19 @@ export const metadata: Metadata = {
|
|||
title: "Khoj AI - Agents",
|
||||
description: "Find a specialized agent that can help you address more specific needs.",
|
||||
icons: {
|
||||
icon: '/static/favicon.ico',
|
||||
icon: "/static/favicon.ico",
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<meta httpEquiv="Content-Security-Policy"
|
||||
<meta
|
||||
httpEquiv="Content-Security-Policy"
|
||||
content="default-src 'self' https://assets.khoj.dev;
|
||||
media-src * blob:;
|
||||
script-src 'self' https://assets.khoj.dev 'unsafe-inline' 'unsafe-eval';
|
||||
|
@ -28,10 +29,9 @@ export default function RootLayout({
|
|||
img-src 'self' data: https://*.khoj.dev https://*.googleusercontent.com https://*.google.com/ https://*.gstatic.com;
|
||||
font-src 'self' https://assets.khoj.dev https://fonts.gstatic.com;
|
||||
child-src 'none';
|
||||
object-src 'none';"></meta>
|
||||
<body className={inter.className}>
|
||||
{children}
|
||||
</body>
|
||||
object-src 'none';"
|
||||
></meta>
|
||||
<body className={inter.className}>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,37 +1,50 @@
|
|||
'use client'
|
||||
"use client";
|
||||
|
||||
import styles from './agents.module.css';
|
||||
import styles from "./agents.module.css";
|
||||
|
||||
import Image from 'next/image';
|
||||
import useSWR from 'swr';
|
||||
import Image from "next/image";
|
||||
import useSWR from "swr";
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { useAuthenticatedData, UserProfile } from '../common/auth';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip"
|
||||
import { useAuthenticatedData, UserProfile } from "../common/auth";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
|
||||
import { PaperPlaneTilt, Lightning, Plus } from "@phosphor-icons/react";
|
||||
|
||||
import {
|
||||
PaperPlaneTilt,
|
||||
Lightning,
|
||||
Plus,
|
||||
} from "@phosphor-icons/react";
|
||||
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTrigger } from '@/components/ui/dialog';
|
||||
import { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger } from '@/components/ui/drawer';
|
||||
import LoginPrompt from '../components/loginPrompt/loginPrompt';
|
||||
import { InlineLoading } from '../components/loading/loading';
|
||||
import SidePanel from '../components/sidePanel/chatHistorySidePanel';
|
||||
import NavMenu from '../components/navMenu/navMenu';
|
||||
import { getIconFromIconName } from '../common/iconUtils';
|
||||
import { convertColorToTextClass } from '../common/colorUtils';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Drawer,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerDescription,
|
||||
DrawerFooter,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
DrawerTrigger,
|
||||
} from "@/components/ui/drawer";
|
||||
import LoginPrompt from "../components/loginPrompt/loginPrompt";
|
||||
import { InlineLoading } from "../components/loading/loading";
|
||||
import SidePanel from "../components/sidePanel/chatHistorySidePanel";
|
||||
import NavMenu from "../components/navMenu/navMenu";
|
||||
import { getIconFromIconName } from "../common/iconUtils";
|
||||
import { convertColorToTextClass } from "../common/colorUtils";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
|
||||
export interface AgentData {
|
||||
slug: string;
|
||||
|
@ -43,7 +56,6 @@ export interface AgentData {
|
|||
}
|
||||
|
||||
async function openChat(slug: string, userData: UserProfile | null) {
|
||||
|
||||
const unauthenticatedRedirectUrl = `/login?next=/agents?agent=${slug}`;
|
||||
if (!userData) {
|
||||
window.location.href = unauthenticatedRedirectUrl;
|
||||
|
@ -61,7 +73,11 @@ async function openChat(slug: string, userData: UserProfile | null) {
|
|||
}
|
||||
}
|
||||
|
||||
const agentsFetcher = () => window.fetch('/api/agents').then(res => res.json()).catch(err => console.log(err));
|
||||
const agentsFetcher = () =>
|
||||
window
|
||||
.fetch("/api/agents")
|
||||
.then((res) => res.json())
|
||||
.catch((err) => console.log(err));
|
||||
|
||||
interface AgentCardProps {
|
||||
data: AgentData;
|
||||
|
@ -69,171 +85,195 @@ interface AgentCardProps {
|
|||
isMobileWidth: boolean;
|
||||
}
|
||||
|
||||
|
||||
function AgentCard(props: AgentCardProps) {
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const agentSlug = searchParams.get('agent');
|
||||
const agentSlug = searchParams.get("agent");
|
||||
const [showModal, setShowModal] = useState(agentSlug === props.data.slug);
|
||||
const [showLoginPrompt, setShowLoginPrompt] = useState(false);
|
||||
|
||||
const userData = props.userProfile;
|
||||
|
||||
if (showModal) {
|
||||
window.history.pushState({}, `Khoj AI - Agent ${props.data.slug}`, `/agents?agent=${props.data.slug}`);
|
||||
window.history.pushState(
|
||||
{},
|
||||
`Khoj AI - Agent ${props.data.slug}`,
|
||||
`/agents?agent=${props.data.slug}`,
|
||||
);
|
||||
}
|
||||
|
||||
const stylingString = convertColorToTextClass(props.data.color);
|
||||
|
||||
return (
|
||||
<Card className={`shadow-sm bg-gradient-to-b from-white 20% to-${props.data.color ? props.data.color : "gray"}-100/50 dark:from-[hsl(var(--background))] dark:to-${props.data.color ? props.data.color : "gray"}-950/50 rounded-xl hover:shadow-md`}>
|
||||
{
|
||||
showLoginPrompt &&
|
||||
<Card
|
||||
className={`shadow-sm bg-gradient-to-b from-white 20% to-${props.data.color ? props.data.color : "gray"}-100/50 dark:from-[hsl(var(--background))] dark:to-${props.data.color ? props.data.color : "gray"}-950/50 rounded-xl hover:shadow-md`}
|
||||
>
|
||||
{showLoginPrompt && (
|
||||
<LoginPrompt
|
||||
loginRedirectMessage={`Sign in to start chatting with ${props.data.name}`}
|
||||
onOpenChange={setShowLoginPrompt} />
|
||||
}
|
||||
onOpenChange={setShowLoginPrompt}
|
||||
/>
|
||||
)}
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
{
|
||||
!props.isMobileWidth ?
|
||||
<Dialog
|
||||
open={showModal}
|
||||
onOpenChange={() => {
|
||||
setShowModal(!showModal);
|
||||
window.history.pushState({}, `Khoj AI - Agents`, `/agents`);
|
||||
}}>
|
||||
<DialogTrigger>
|
||||
<div className='flex items-center relative top-2'>
|
||||
{
|
||||
getIconFromIconName(props.data.icon, props.data.color) || <Image
|
||||
{!props.isMobileWidth ? (
|
||||
<Dialog
|
||||
open={showModal}
|
||||
onOpenChange={() => {
|
||||
setShowModal(!showModal);
|
||||
window.history.pushState({}, `Khoj AI - Agents`, `/agents`);
|
||||
}}
|
||||
>
|
||||
<DialogTrigger>
|
||||
<div className="flex items-center relative top-2">
|
||||
{getIconFromIconName(props.data.icon, props.data.color) || (
|
||||
<Image
|
||||
src={props.data.avatar}
|
||||
alt={props.data.name}
|
||||
width={50}
|
||||
height={50}
|
||||
/>
|
||||
)}
|
||||
{props.data.name}
|
||||
</div>
|
||||
</DialogTrigger>
|
||||
<div className="float-right">
|
||||
{props.userProfile ? (
|
||||
<Button
|
||||
className={`bg-[hsl(var(--background))] w-14 h-14 rounded-xl border dark:border-neutral-700 shadow-sm hover:bg-stone-100 dark:hover:bg-neutral-900`}
|
||||
onClick={() => openChat(props.data.slug, userData)}
|
||||
>
|
||||
<PaperPlaneTilt
|
||||
className={`w-6 h-6 ${convertColorToTextClass(props.data.color)}`}
|
||||
/>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className={`bg-[hsl(var(--background))] w-14 h-14 rounded-xl border dark:border-neutral-700 shadow-sm hover:bg-stone-100 dark:hover:bg-neutral-900`}
|
||||
onClick={() => setShowLoginPrompt(true)}
|
||||
>
|
||||
<PaperPlaneTilt
|
||||
className={`w-6 h-6 ${convertColorToTextClass(props.data.color)}`}
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<DialogContent className="whitespace-pre-line max-h-[80vh]">
|
||||
<DialogHeader>
|
||||
<div className="flex items-center">
|
||||
{getIconFromIconName(props.data.icon, props.data.color) || (
|
||||
<Image
|
||||
src={props.data.avatar}
|
||||
alt={props.data.name}
|
||||
width={50}
|
||||
width={32}
|
||||
height={50}
|
||||
/>
|
||||
}
|
||||
{props.data.name}
|
||||
)}
|
||||
<p className="font-bold text-lg">{props.data.name}</p>
|
||||
</div>
|
||||
</DialogTrigger>
|
||||
<div className="float-right">
|
||||
{props.userProfile ? (
|
||||
<Button
|
||||
className={`bg-[hsl(var(--background))] w-14 h-14 rounded-xl border dark:border-neutral-700 shadow-sm hover:bg-stone-100 dark:hover:bg-neutral-900`}
|
||||
onClick={() => openChat(props.data.slug, userData)}>
|
||||
<PaperPlaneTilt className={`w-6 h-6 ${convertColorToTextClass(props.data.color)}`} />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className={`bg-[hsl(var(--background))] w-14 h-14 rounded-xl border dark:border-neutral-700 shadow-sm hover:bg-stone-100 dark:hover:bg-neutral-900`}
|
||||
onClick={() => setShowLoginPrompt(true)}>
|
||||
<PaperPlaneTilt className={`w-6 h-6 ${convertColorToTextClass(props.data.color)}`} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<DialogContent className='whitespace-pre-line max-h-[80vh]'>
|
||||
<DialogHeader>
|
||||
<div className='flex items-center'>
|
||||
{
|
||||
getIconFromIconName(props.data.icon, props.data.color) || <Image
|
||||
src={props.data.avatar}
|
||||
alt={props.data.name}
|
||||
width={32}
|
||||
height={50}
|
||||
/>
|
||||
}
|
||||
<p className="font-bold text-lg">{props.data.name}</p>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
<div className="max-h-[60vh] overflow-y-scroll text-neutral-500 dark:text-white">
|
||||
{props.data.persona}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
className={`pt-6 pb-6 ${stylingString} bg-white dark:bg-[hsl(var(--background))] text-neutral-500 dark:text-white border-2 border-stone-100 shadow-sm rounded-xl hover:bg-stone-100 dark:hover:bg-neutral-900 dark:border-neutral-700`}
|
||||
onClick={() => {
|
||||
openChat(props.data.slug, userData);
|
||||
setShowModal(false);
|
||||
}}>
|
||||
<PaperPlaneTilt className={`w-6 h-6 m-2 ${convertColorToTextClass(props.data.color)}`} />
|
||||
Start Chatting
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
:
|
||||
<Drawer
|
||||
open={showModal}
|
||||
onOpenChange={(open) => {
|
||||
setShowModal(open);
|
||||
window.history.pushState({}, `Khoj AI - Agents`, `/agents`);
|
||||
}}>
|
||||
<DrawerTrigger>
|
||||
<div className='flex items-center'>
|
||||
{
|
||||
getIconFromIconName(props.data.icon, props.data.color) || <Image
|
||||
src={props.data.avatar}
|
||||
alt={props.data.name}
|
||||
width={50}
|
||||
height={50}
|
||||
/>
|
||||
}
|
||||
{props.data.name}
|
||||
</div>
|
||||
</DrawerTrigger>
|
||||
<div className="float-right">
|
||||
{props.userProfile ? (
|
||||
<Button
|
||||
className={`bg-[hsl(var(--background))] w-14 h-14 rounded-xl border dark:border-neutral-700 shadow-sm hover:bg-stone-100`}
|
||||
onClick={() => openChat(props.data.slug, userData)}>
|
||||
<PaperPlaneTilt className={`w-6 h-6 ${convertColorToTextClass(props.data.color)}`} />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className={`bg-[hsl(var(--background))] w-14 h-14 rounded-xl border dark:border-neutral-700 shadow-sm`}
|
||||
onClick={() => setShowLoginPrompt(true)}>
|
||||
<PaperPlaneTilt className={`w-6 h-6 ${convertColorToTextClass(props.data.color)}`} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<DrawerContent className='whitespace-pre-line p-2'>
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>{props.data.name}</DrawerTitle>
|
||||
<DrawerDescription>Full Prompt</DrawerDescription>
|
||||
</DrawerHeader>
|
||||
</DialogHeader>
|
||||
<div className="max-h-[60vh] overflow-y-scroll text-neutral-500 dark:text-white">
|
||||
{props.data.persona}
|
||||
<DrawerFooter>
|
||||
<DrawerClose>
|
||||
Done
|
||||
</DrawerClose>
|
||||
</DrawerFooter>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
className={`pt-6 pb-6 ${stylingString} bg-white dark:bg-[hsl(var(--background))] text-neutral-500 dark:text-white border-2 border-stone-100 shadow-sm rounded-xl hover:bg-stone-100 dark:hover:bg-neutral-900 dark:border-neutral-700`}
|
||||
onClick={() => {
|
||||
openChat(props.data.slug, userData);
|
||||
setShowModal(false);
|
||||
}}
|
||||
>
|
||||
<PaperPlaneTilt
|
||||
className={`w-6 h-6 m-2 ${convertColorToTextClass(props.data.color)}`}
|
||||
/>
|
||||
Start Chatting
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
) : (
|
||||
<Drawer
|
||||
open={showModal}
|
||||
onOpenChange={(open) => {
|
||||
setShowModal(open);
|
||||
window.history.pushState({}, `Khoj AI - Agents`, `/agents`);
|
||||
}}
|
||||
>
|
||||
<DrawerTrigger>
|
||||
<div className="flex items-center">
|
||||
{getIconFromIconName(props.data.icon, props.data.color) || (
|
||||
<Image
|
||||
src={props.data.avatar}
|
||||
alt={props.data.name}
|
||||
width={50}
|
||||
height={50}
|
||||
/>
|
||||
)}
|
||||
{props.data.name}
|
||||
</div>
|
||||
</DrawerTrigger>
|
||||
<div className="float-right">
|
||||
{props.userProfile ? (
|
||||
<Button
|
||||
className={`bg-[hsl(var(--background))] w-14 h-14 rounded-xl border dark:border-neutral-700 shadow-sm hover:bg-stone-100`}
|
||||
onClick={() => openChat(props.data.slug, userData)}
|
||||
>
|
||||
<PaperPlaneTilt
|
||||
className={`w-6 h-6 ${convertColorToTextClass(props.data.color)}`}
|
||||
/>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className={`bg-[hsl(var(--background))] w-14 h-14 rounded-xl border dark:border-neutral-700 shadow-sm`}
|
||||
onClick={() => setShowLoginPrompt(true)}
|
||||
>
|
||||
<PaperPlaneTilt
|
||||
className={`w-6 h-6 ${convertColorToTextClass(props.data.color)}`}
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<DrawerContent className="whitespace-pre-line p-2">
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>{props.data.name}</DrawerTitle>
|
||||
<DrawerDescription>Full Prompt</DrawerDescription>
|
||||
</DrawerHeader>
|
||||
{props.data.persona}
|
||||
<DrawerFooter>
|
||||
<DrawerClose>Done</DrawerClose>
|
||||
</DrawerFooter>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className={styles.agentPersonality}>
|
||||
<button className={`${styles.infoButton} text-neutral-500 dark:text-white`} onClick={() => setShowModal(true)}>
|
||||
<button
|
||||
className={`${styles.infoButton} text-neutral-500 dark:text-white`}
|
||||
onClick={() => setShowModal(true)}
|
||||
>
|
||||
<p>{props.data.persona}</p>
|
||||
</button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default function Agents() {
|
||||
const { data, error } = useSWR<AgentData[]>('agents', agentsFetcher, { revalidateOnFocus: false });
|
||||
const { data, error } = useSWR<AgentData[]>("agents", agentsFetcher, {
|
||||
revalidateOnFocus: false,
|
||||
});
|
||||
const authenticatedData = useAuthenticatedData();
|
||||
const [isMobileWidth, setIsMobileWidth] = useState(false);
|
||||
const [showLoginPrompt, setShowLoginPrompt] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
if (typeof window !== "undefined") {
|
||||
setIsMobileWidth(window.innerWidth < 768);
|
||||
}
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
window.addEventListener("resize", () => {
|
||||
setIsMobileWidth(window.innerWidth < 768);
|
||||
});
|
||||
}, []);
|
||||
|
@ -241,12 +281,8 @@ export default function Agents() {
|
|||
if (error) {
|
||||
return (
|
||||
<main className={styles.main}>
|
||||
<div className={`${styles.titleBar} text-5xl`}>
|
||||
Agents
|
||||
</div>
|
||||
<div className={styles.agentList}>
|
||||
Error loading agents
|
||||
</div>
|
||||
<div className={`${styles.titleBar} text-5xl`}>Agents</div>
|
||||
<div className={styles.agentList}>Error loading agents</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
@ -279,7 +315,7 @@ export default function Agents() {
|
|||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<div className="flex flex-row">
|
||||
<Plus className='pr-2 w-6 h-6' />
|
||||
<Plus className="pr-2 w-6 h-6" />
|
||||
<p className="pr-2">Create Agent</p>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
|
@ -290,21 +326,27 @@ export default function Agents() {
|
|||
</TooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
showLoginPrompt &&
|
||||
{showLoginPrompt && (
|
||||
<LoginPrompt
|
||||
loginRedirectMessage="Sign in to start chatting with a specialized agent"
|
||||
onOpenChange={setShowLoginPrompt} />
|
||||
}
|
||||
<Alert className='bg-secondary border-none my-4'>
|
||||
onOpenChange={setShowLoginPrompt}
|
||||
/>
|
||||
)}
|
||||
<Alert className="bg-secondary border-none my-4">
|
||||
<AlertDescription>
|
||||
<Lightning weight={'fill'} className='h-4 w-4 text-purple-400 inline' />
|
||||
<span className='font-bold'>How it works</span> Use any of these specialized personas to tune your conversation to your needs.
|
||||
<Lightning weight={"fill"} className="h-4 w-4 text-purple-400 inline" />
|
||||
<span className="font-bold">How it works</span> Use any of these
|
||||
specialized personas to tune your conversation to your needs.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<div className={`${styles.agentList}`}>
|
||||
{data.map(agent => (
|
||||
<AgentCard key={agent.slug} data={agent} userProfile={authenticatedData} isMobileWidth={isMobileWidth} />
|
||||
{data.map((agent) => (
|
||||
<AgentCard
|
||||
key={agent.slug}
|
||||
data={agent}
|
||||
userProfile={authenticatedData}
|
||||
isMobileWidth={isMobileWidth}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -3,12 +3,11 @@ import { Toaster } from "@/components/ui/toaster";
|
|||
|
||||
import "../globals.css";
|
||||
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Khoj AI - Automations",
|
||||
description: "Use Autoomations with Khoj to simplify the process of running repetitive tasks.",
|
||||
icons: {
|
||||
icon: '/static/favicon.ico',
|
||||
icon: "/static/favicon.ico",
|
||||
},
|
||||
};
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -6,21 +6,23 @@ const inter = Noto_Sans({ subsets: ["latin"] });
|
|||
|
||||
export const metadata: Metadata = {
|
||||
title: "Khoj AI - Chat",
|
||||
description: "Ask anything. Khoj will use the internet and your docs to answer, paint and even automate stuff for you.",
|
||||
description:
|
||||
"Ask anything. Khoj will use the internet and your docs to answer, paint and even automate stuff for you.",
|
||||
icons: {
|
||||
icon: '/static/favicon.ico',
|
||||
icon: "/static/favicon.ico",
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<meta httpEquiv="Content-Security-Policy"
|
||||
content="default-src 'self' https://assets.khoj.dev;
|
||||
return (
|
||||
<html lang="en">
|
||||
<meta
|
||||
httpEquiv="Content-Security-Policy"
|
||||
content="default-src 'self' https://assets.khoj.dev;
|
||||
media-src * blob:;
|
||||
script-src 'self' https://assets.khoj.dev 'unsafe-inline' 'unsafe-eval';
|
||||
connect-src 'self' https://ipapi.co/json ws://localhost:42110;
|
||||
|
@ -28,10 +30,9 @@ export default function RootLayout({
|
|||
img-src 'self' data: https://*.khoj.dev https://*.googleusercontent.com https://*.google.com/ https://*.gstatic.com;
|
||||
font-src 'self' https://assets.khoj.dev https://fonts.gstatic.com;
|
||||
child-src 'none';
|
||||
object-src 'none';"></meta>
|
||||
<body className={inter.className}>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
object-src 'none';"
|
||||
></meta>
|
||||
<body className={inter.className}>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,22 +1,22 @@
|
|||
'use client'
|
||||
"use client";
|
||||
|
||||
import styles from './chat.module.css';
|
||||
import React, { Suspense, useEffect, useState } from 'react';
|
||||
import styles from "./chat.module.css";
|
||||
import React, { Suspense, useEffect, useState } from "react";
|
||||
|
||||
import SidePanel from '../components/sidePanel/chatHistorySidePanel';
|
||||
import ChatHistory from '../components/chatHistory/chatHistory';
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
import Loading from '../components/loading/loading';
|
||||
import SidePanel from "../components/sidePanel/chatHistorySidePanel";
|
||||
import ChatHistory from "../components/chatHistory/chatHistory";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import Loading from "../components/loading/loading";
|
||||
|
||||
import { processMessageChunk } from '../common/chatFunctions';
|
||||
import { processMessageChunk } from "../common/chatFunctions";
|
||||
|
||||
import 'katex/dist/katex.min.css';
|
||||
import "katex/dist/katex.min.css";
|
||||
|
||||
import { Context, OnlineContext, StreamMessage } from '../components/chatMessage/chatMessage';
|
||||
import { useIPLocationData, welcomeConsole } from '../common/utils';
|
||||
import ChatInputArea, { ChatOptions } from '../components/chatInputArea/chatInputArea';
|
||||
import { useAuthenticatedData } from '../common/auth';
|
||||
import { AgentData } from '../agents/page';
|
||||
import { Context, OnlineContext, StreamMessage } from "../components/chatMessage/chatMessage";
|
||||
import { useIPLocationData, welcomeConsole } from "../common/utils";
|
||||
import ChatInputArea, { ChatOptions } from "../components/chatInputArea/chatInputArea";
|
||||
import { useAuthenticatedData } from "../common/auth";
|
||||
import { AgentData } from "../agents/page";
|
||||
|
||||
interface ChatBodyDataProps {
|
||||
chatOptionsData: ChatOptions | null;
|
||||
|
@ -31,8 +31,8 @@ interface ChatBodyDataProps {
|
|||
|
||||
function ChatBodyData(props: ChatBodyDataProps) {
|
||||
const searchParams = useSearchParams();
|
||||
const conversationId = searchParams.get('conversationId');
|
||||
const [message, setMessage] = useState('');
|
||||
const conversationId = searchParams.get("conversationId");
|
||||
const [message, setMessage] = useState("");
|
||||
const [processingMessage, setProcessingMessage] = useState(false);
|
||||
const [agentMetadata, setAgentMetadata] = useState<AgentData | null>(null);
|
||||
|
||||
|
@ -61,17 +61,19 @@ function ChatBodyData(props: ChatBodyDataProps) {
|
|||
}, [conversationId, onConversationIdChange]);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.streamedMessages &&
|
||||
if (
|
||||
props.streamedMessages &&
|
||||
props.streamedMessages.length > 0 &&
|
||||
props.streamedMessages[props.streamedMessages.length - 1].completed) {
|
||||
props.streamedMessages[props.streamedMessages.length - 1].completed
|
||||
) {
|
||||
setProcessingMessage(false);
|
||||
} else {
|
||||
setMessage('');
|
||||
setMessage("");
|
||||
}
|
||||
}, [props.streamedMessages]);
|
||||
|
||||
if (!conversationId) {
|
||||
window.location.href = '/';
|
||||
window.location.href = "/";
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -82,11 +84,13 @@ function ChatBodyData(props: ChatBodyDataProps) {
|
|||
conversationId={conversationId}
|
||||
setTitle={props.setTitle}
|
||||
setAgent={setAgentMetadata}
|
||||
pendingMessage={processingMessage ? message : ''}
|
||||
pendingMessage={processingMessage ? message : ""}
|
||||
incomingMessages={props.streamedMessages}
|
||||
/>
|
||||
</div>
|
||||
<div className={`${styles.inputBox} p-1 md:px-2 shadow-md bg-background align-middle items-center justify-center dark:bg-neutral-700 dark:border-0 dark:shadow-sm rounded-t-2xl rounded-b-none md:rounded-xl`}>
|
||||
<div
|
||||
className={`${styles.inputBox} p-1 md:px-2 shadow-md bg-background align-middle items-center justify-center dark:bg-neutral-700 dark:border-0 dark:shadow-sm rounded-t-2xl rounded-b-none md:rounded-xl`}
|
||||
>
|
||||
<ChatInputArea
|
||||
agentColor={agentMetadata?.color}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
|
@ -95,20 +99,21 @@ function ChatBodyData(props: ChatBodyDataProps) {
|
|||
chatOptionsData={props.chatOptionsData}
|
||||
conversationId={conversationId}
|
||||
isMobileWidth={props.isMobileWidth}
|
||||
setUploadedFiles={props.setUploadedFiles} />
|
||||
setUploadedFiles={props.setUploadedFiles}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Chat() {
|
||||
const defaultTitle = 'Khoj AI - Chat';
|
||||
const defaultTitle = "Khoj AI - Chat";
|
||||
const [chatOptionsData, setChatOptionsData] = useState<ChatOptions | null>(null);
|
||||
const [isLoading, setLoading] = useState(true);
|
||||
const [title, setTitle] = useState(defaultTitle);
|
||||
const [conversationId, setConversationID] = useState<string | null>(null);
|
||||
const [messages, setMessages] = useState<StreamMessage[]>([]);
|
||||
const [queryToProcess, setQueryToProcess] = useState<string>('');
|
||||
const [queryToProcess, setQueryToProcess] = useState<string>("");
|
||||
const [processQuerySignal, setProcessQuerySignal] = useState(false);
|
||||
const [uploadedFiles, setUploadedFiles] = useState<string[]>([]);
|
||||
const [isMobileWidth, setIsMobileWidth] = useState(false);
|
||||
|
@ -116,8 +121,8 @@ export default function Chat() {
|
|||
const authenticatedData = useAuthenticatedData();
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/chat/options')
|
||||
.then(response => response.json())
|
||||
fetch("/api/chat/options")
|
||||
.then((response) => response.json())
|
||||
.then((data: ChatOptions) => {
|
||||
setLoading(false);
|
||||
// Render chat options, if any
|
||||
|
@ -125,7 +130,7 @@ export default function Chat() {
|
|||
setChatOptionsData(data);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
return;
|
||||
});
|
||||
|
@ -134,10 +139,9 @@ export default function Chat() {
|
|||
|
||||
setIsMobileWidth(window.innerWidth < 786);
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
window.addEventListener("resize", () => {
|
||||
setIsMobileWidth(window.innerWidth < 786);
|
||||
});
|
||||
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -148,10 +152,10 @@ export default function Chat() {
|
|||
context: [],
|
||||
onlineContext: {},
|
||||
completed: false,
|
||||
timestamp: (new Date()).toISOString(),
|
||||
timestamp: new Date().toISOString(),
|
||||
rawQuery: queryToProcess || "",
|
||||
};
|
||||
setMessages(prevMessages => [...prevMessages, newStreamMessage]);
|
||||
setMessages((prevMessages) => [...prevMessages, newStreamMessage]);
|
||||
setProcessQuerySignal(true);
|
||||
}
|
||||
}, [queryToProcess]);
|
||||
|
@ -168,7 +172,7 @@ export default function Chat() {
|
|||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
const eventDelimiter = '␃🔚␗';
|
||||
const eventDelimiter = "␃🔚␗";
|
||||
let buffer = "";
|
||||
|
||||
// Track context used for chat response
|
||||
|
@ -178,7 +182,7 @@ export default function Chat() {
|
|||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
setQueryToProcess('');
|
||||
setQueryToProcess("");
|
||||
setProcessQuerySignal(false);
|
||||
break;
|
||||
}
|
||||
|
@ -191,7 +195,7 @@ export default function Chat() {
|
|||
const event = buffer.slice(0, newEventIndex);
|
||||
buffer = buffer.slice(newEventIndex + eventDelimiter.length);
|
||||
if (event) {
|
||||
const currentMessage = messages.find(message => !message.completed);
|
||||
const currentMessage = messages.find((message) => !message.completed);
|
||||
|
||||
if (!currentMessage) {
|
||||
console.error("No current message found");
|
||||
|
@ -199,7 +203,12 @@ export default function Chat() {
|
|||
}
|
||||
|
||||
// Track context used for chat response. References are rendered at the end of the chat
|
||||
({ context, onlineContext } = processMessageChunk(event, currentMessage, context, onlineContext));
|
||||
({ context, onlineContext } = processMessageChunk(
|
||||
event,
|
||||
currentMessage,
|
||||
context,
|
||||
onlineContext,
|
||||
));
|
||||
|
||||
setMessages([...messages]);
|
||||
}
|
||||
|
@ -232,7 +241,7 @@ export default function Chat() {
|
|||
return (
|
||||
<div className={`${styles.main} ${styles.chatLayout}`}>
|
||||
<title>
|
||||
{`${defaultTitle}${(!!title && title !== defaultTitle)? `: ${title}` : ''}`}
|
||||
{`${defaultTitle}${!!title && title !== defaultTitle ? `: ${title}` : ""}`}
|
||||
</title>
|
||||
<div>
|
||||
<SidePanel
|
||||
|
@ -243,12 +252,19 @@ export default function Chat() {
|
|||
</div>
|
||||
<div className={styles.chatBox}>
|
||||
<div className={styles.chatBoxBody}>
|
||||
{
|
||||
!isMobileWidth &&
|
||||
<div className={`text-nowrap text-ellipsis overflow-hidden max-w-screen-md grid items-top font-bold mr-8`}>
|
||||
{title && <h2 className={`text-lg text-ellipsis whitespace-nowrap overflow-x-hidden pt-6`}>{title}</h2>}
|
||||
{!isMobileWidth && (
|
||||
<div
|
||||
className={`text-nowrap text-ellipsis overflow-hidden max-w-screen-md grid items-top font-bold mr-8`}
|
||||
>
|
||||
{title && (
|
||||
<h2
|
||||
className={`text-lg text-ellipsis whitespace-nowrap overflow-x-hidden pt-6`}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
<Suspense fallback={<Loading />}>
|
||||
<ChatBodyData
|
||||
isLoggedIn={authenticatedData !== null}
|
||||
|
@ -258,10 +274,11 @@ export default function Chat() {
|
|||
setQueryToProcess={setQueryToProcess}
|
||||
setUploadedFiles={setUploadedFiles}
|
||||
isMobileWidth={isMobileWidth}
|
||||
onConversationIdChange={handleConversationIdChange} />
|
||||
onConversationIdChange={handleConversationIdChange}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
'use client'
|
||||
"use client";
|
||||
|
||||
import useSWR from 'swr'
|
||||
import useSWR from "swr";
|
||||
|
||||
export interface UserProfile {
|
||||
email: string;
|
||||
|
@ -12,14 +12,17 @@ export interface UserProfile {
|
|||
}
|
||||
|
||||
const fetcher = (url: string) =>
|
||||
window.fetch(url)
|
||||
.then(res => res.json())
|
||||
.catch(err => console.warn(err));
|
||||
window
|
||||
.fetch(url)
|
||||
.then((res) => res.json())
|
||||
.catch((err) => console.warn(err));
|
||||
|
||||
export function useAuthenticatedData() {
|
||||
const { data, error } = useSWR<UserProfile>('/api/v1/user', fetcher, { revalidateOnFocus: false });
|
||||
const { data, error } = useSWR<UserProfile>("/api/v1/user", fetcher, {
|
||||
revalidateOnFocus: false,
|
||||
});
|
||||
|
||||
if (error || !data || data.detail === 'Forbidden') return null;
|
||||
if (error || !data || data.detail === "Forbidden") return null;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
@ -68,12 +71,16 @@ export interface UserConfig {
|
|||
detail: string;
|
||||
}
|
||||
|
||||
|
||||
export function useUserConfig(detailed: boolean = false) {
|
||||
const url = `/api/settings?detailed=${detailed}`;
|
||||
const { data: userConfig, error, isLoading: isLoadingUserConfig } = useSWR<UserConfig>(url, fetcher, { revalidateOnFocus: false });
|
||||
const {
|
||||
data: userConfig,
|
||||
error,
|
||||
isLoading: isLoadingUserConfig,
|
||||
} = useSWR<UserConfig>(url, fetcher, { revalidateOnFocus: false });
|
||||
|
||||
if (error || !userConfig || userConfig?.detail === 'Forbidden') return {userConfig: null, isLoadingUserConfig};
|
||||
if (error || !userConfig || userConfig?.detail === "Forbidden")
|
||||
return { userConfig: null, isLoadingUserConfig };
|
||||
|
||||
return {userConfig, isLoadingUserConfig};
|
||||
return { userConfig, isLoadingUserConfig };
|
||||
}
|
||||
|
|
|
@ -23,25 +23,25 @@ export function convertMessageChunkToJson(chunk: string): MessageChunk {
|
|||
if (!jsonChunk.type) {
|
||||
return {
|
||||
type: "message",
|
||||
data: jsonChunk
|
||||
data: jsonChunk,
|
||||
};
|
||||
}
|
||||
return jsonChunk;
|
||||
} catch (error) {
|
||||
return {
|
||||
type: "message",
|
||||
data: chunk
|
||||
data: chunk,
|
||||
};
|
||||
}
|
||||
} else if (chunk.length > 0) {
|
||||
return {
|
||||
type: "message",
|
||||
data: chunk
|
||||
data: chunk,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: "message",
|
||||
data: ""
|
||||
data: "",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -62,11 +62,11 @@ export function processMessageChunk(
|
|||
rawChunk: string,
|
||||
currentMessage: StreamMessage,
|
||||
context: Context[] = [],
|
||||
onlineContext: OnlineContext = {}): { context: Context[], onlineContext: OnlineContext } {
|
||||
|
||||
onlineContext: OnlineContext = {},
|
||||
): { context: Context[]; onlineContext: OnlineContext } {
|
||||
const chunk = convertMessageChunkToJson(rawChunk);
|
||||
|
||||
if (!currentMessage || !chunk || !chunk.type) return {context, onlineContext};
|
||||
if (!currentMessage || !chunk || !chunk.type) return { context, onlineContext };
|
||||
|
||||
if (chunk.type === "status") {
|
||||
console.log(`status: ${chunk.data}`);
|
||||
|
@ -77,12 +77,16 @@ export function processMessageChunk(
|
|||
|
||||
if (references.context) context = references.context;
|
||||
if (references.onlineContext) onlineContext = references.onlineContext;
|
||||
return {context, onlineContext}
|
||||
return { context, onlineContext };
|
||||
} else if (chunk.type === "message") {
|
||||
const chunkData = chunk.data;
|
||||
if (chunkData !== null && typeof chunkData === 'object') {
|
||||
if (chunkData !== null && typeof chunkData === "object") {
|
||||
currentMessage.rawResponse += handleJsonResponse(chunkData);
|
||||
} else if (typeof chunkData === 'string' && chunkData.trim()?.startsWith("{") && chunkData.trim()?.endsWith("}")) {
|
||||
} else if (
|
||||
typeof chunkData === "string" &&
|
||||
chunkData.trim()?.startsWith("{") &&
|
||||
chunkData.trim()?.endsWith("}")
|
||||
) {
|
||||
try {
|
||||
const jsonData = JSON.parse(chunkData.trim());
|
||||
currentMessage.rawResponse += handleJsonResponse(jsonData);
|
||||
|
@ -104,11 +108,10 @@ export function processMessageChunk(
|
|||
// Mark current message streaming as completed
|
||||
currentMessage.completed = true;
|
||||
}
|
||||
return {context, onlineContext};
|
||||
return { context, onlineContext };
|
||||
}
|
||||
|
||||
export function handleImageResponse(imageJson: any, liveStream: boolean): ResponseWithReferences {
|
||||
|
||||
let rawResponse = "";
|
||||
|
||||
if (imageJson.image) {
|
||||
|
@ -146,39 +149,38 @@ export function handleImageResponse(imageJson: any, liveStream: boolean): Respon
|
|||
return reference;
|
||||
}
|
||||
|
||||
|
||||
export function modifyFileFilterForConversation(
|
||||
conversationId: string | null,
|
||||
filenames: string[],
|
||||
setAddedFiles: (files: string[]) => void,
|
||||
mode: 'add' | 'remove') {
|
||||
|
||||
mode: "add" | "remove",
|
||||
) {
|
||||
if (!conversationId) {
|
||||
console.error("No conversation ID provided");
|
||||
return;
|
||||
}
|
||||
|
||||
const method = mode === 'add' ? 'POST' : 'DELETE';
|
||||
const method = mode === "add" ? "POST" : "DELETE";
|
||||
|
||||
const body = {
|
||||
conversation_id: conversationId,
|
||||
filenames: filenames,
|
||||
}
|
||||
};
|
||||
const addUrl = `/api/chat/conversation/file-filters/bulk`;
|
||||
|
||||
fetch(addUrl, {
|
||||
method: method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
console.log("ADDEDFILES DATA: ", data);
|
||||
setAddedFiles(data);
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
return;
|
||||
});
|
||||
|
@ -186,8 +188,11 @@ export function modifyFileFilterForConversation(
|
|||
|
||||
export async function createNewConversation(slug: string) {
|
||||
try {
|
||||
const response = await fetch(`/api/chat/sessions?client=web&agent_slug=${slug}`, { method: "POST" });
|
||||
if (!response.ok) throw new Error(`Failed to fetch chat sessions with status: ${response.status}`);
|
||||
const response = await fetch(`/api/chat/sessions?client=web&agent_slug=${slug}`, {
|
||||
method: "POST",
|
||||
});
|
||||
if (!response.ok)
|
||||
throw new Error(`Failed to fetch chat sessions with status: ${response.status}`);
|
||||
const data = await response.json();
|
||||
const conversationID = data.conversation_id;
|
||||
if (!conversationID) throw new Error("Conversation ID not found in response");
|
||||
|
@ -204,22 +209,32 @@ export function uploadDataForIndexing(
|
|||
setUploading: (uploading: boolean) => void,
|
||||
setError: (error: string) => void,
|
||||
setUploadedFiles?: (files: string[]) => void,
|
||||
conversationId?: string | null) {
|
||||
|
||||
const allowedExtensions = ['text/org', 'text/markdown', 'text/plain', 'text/html', 'application/pdf', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'];
|
||||
const allowedFileEndings = ['org', 'md', 'txt', 'html', 'pdf', 'docx'];
|
||||
conversationId?: string | null,
|
||||
) {
|
||||
const allowedExtensions = [
|
||||
"text/org",
|
||||
"text/markdown",
|
||||
"text/plain",
|
||||
"text/html",
|
||||
"application/pdf",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
];
|
||||
const allowedFileEndings = ["org", "md", "txt", "html", "pdf", "docx"];
|
||||
const badFiles: string[] = [];
|
||||
const goodFiles: File[] = [];
|
||||
|
||||
const uploadedFiles: string[] = [];
|
||||
|
||||
for (let file of files) {
|
||||
const fileEnding = file.name.split('.').pop();
|
||||
const fileEnding = file.name.split(".").pop();
|
||||
if (!file || !file.name || !fileEnding) {
|
||||
if (file) {
|
||||
badFiles.push(file.name);
|
||||
}
|
||||
} else if ((!allowedExtensions.includes(file.type) && !allowedFileEndings.includes(fileEnding.toLowerCase()))) {
|
||||
} else if (
|
||||
!allowedExtensions.includes(file.type) &&
|
||||
!allowedFileEndings.includes(fileEnding.toLowerCase())
|
||||
) {
|
||||
badFiles.push(file.name);
|
||||
} else {
|
||||
goodFiles.push(file);
|
||||
|
@ -232,18 +247,16 @@ export function uploadDataForIndexing(
|
|||
}
|
||||
|
||||
if (badFiles.length > 0) {
|
||||
setWarning("The following files are not supported yet:\n" + badFiles.join('\n'));
|
||||
setWarning("The following files are not supported yet:\n" + badFiles.join("\n"));
|
||||
}
|
||||
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
// Create an array of Promises for file reading
|
||||
const fileReadPromises = Array.from(goodFiles).map(file => {
|
||||
const fileReadPromises = Array.from(goodFiles).map((file) => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
let reader = new FileReader();
|
||||
reader.onload = function (event) {
|
||||
|
||||
if (event.target === null) {
|
||||
reject();
|
||||
return;
|
||||
|
@ -253,7 +266,7 @@ export function uploadDataForIndexing(
|
|||
let fileType = file.type;
|
||||
let fileName = file.name;
|
||||
if (fileType === "") {
|
||||
let fileExtension = fileName.split('.').pop();
|
||||
let fileExtension = fileName.split(".").pop();
|
||||
if (fileExtension === "org") {
|
||||
fileType = "text/org";
|
||||
} else if (fileExtension === "md") {
|
||||
|
@ -299,7 +312,12 @@ export function uploadDataForIndexing(
|
|||
for (let file of goodFiles) {
|
||||
uploadedFiles.push(file.name);
|
||||
if (conversationId && setUploadedFiles) {
|
||||
modifyFileFilterForConversation(conversationId, [file.name], setUploadedFiles, 'add');
|
||||
modifyFileFilterForConversation(
|
||||
conversationId,
|
||||
[file.name],
|
||||
setUploadedFiles,
|
||||
"add",
|
||||
);
|
||||
}
|
||||
}
|
||||
if (setUploadedFiles) setUploadedFiles(uploadedFiles);
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
const tailwindColors = [
|
||||
'red',
|
||||
'yellow',
|
||||
'green',
|
||||
'blue',
|
||||
'orange',
|
||||
'purple',
|
||||
'pink',
|
||||
'teal',
|
||||
'cyan',
|
||||
'lime',
|
||||
'indigo',
|
||||
'fuschia',
|
||||
'rose',
|
||||
'sky',
|
||||
'amber',
|
||||
'emerald'
|
||||
"red",
|
||||
"yellow",
|
||||
"green",
|
||||
"blue",
|
||||
"orange",
|
||||
"purple",
|
||||
"pink",
|
||||
"teal",
|
||||
"cyan",
|
||||
"lime",
|
||||
"indigo",
|
||||
"fuschia",
|
||||
"rose",
|
||||
"sky",
|
||||
"amber",
|
||||
"emerald",
|
||||
];
|
||||
|
||||
export function convertColorToTextClass(color: string) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { convertColorToTextClass } from './colorUtils';
|
||||
import React from "react";
|
||||
import { convertColorToTextClass } from "./colorUtils";
|
||||
import {
|
||||
Lightbulb,
|
||||
Robot,
|
||||
|
@ -30,27 +30,68 @@ interface IconMap {
|
|||
}
|
||||
|
||||
const iconMap: IconMap = {
|
||||
Lightbulb: (color: string, width: string, height: string) => <Lightbulb className={`${width} ${height} ${color} mr-2`} />,
|
||||
Robot: (color: string, width: string, height: string) => <Robot className={`${width} ${height} ${color} mr-2`} />,
|
||||
Aperture: (color: string, width: string, height: string) => <Aperture className={`${width} ${height} ${color} mr-2`} />,
|
||||
GraduationCap: (color: string, width: string, height: string) => <GraduationCap className={`${width} ${height} ${color} mr-2`} />,
|
||||
Jeep: (color: string, width: string, height: string) => <Jeep className={`${width} ${height} ${color} mr-2`} />,
|
||||
Island: (color: string, width: string, height: string) => <Island className={`${width} ${height} ${color} mr-2`} />,
|
||||
MathOperations: (color: string, width: string, height: string) => <MathOperations className={`${width} ${height} ${color} mr-2`} />,
|
||||
Asclepius: (color: string, width: string, height: string) => <Asclepius className={`${width} ${height} ${color} mr-2`} />,
|
||||
Couch: (color: string, width: string, height: string) => <Couch className={`${width} ${height} ${color} mr-2`} />,
|
||||
Code: (color: string, width: string, height: string) => <Code className={`${width} ${height} ${color} mr-2`} />,
|
||||
Atom: (color: string, width: string, height: string) => <Atom className={`${width} ${height} ${color} mr-2`} />,
|
||||
ClockCounterClockwise: (color: string, width: string, height: string) => <ClockCounterClockwise className={`${width} ${height} ${color} mr-2`} />,
|
||||
Globe: (color: string, width: string, height: string) => <Globe className={`${width} ${height} ${color} mr-2`} />,
|
||||
Palette: (color: string, width: string, height: string) => <Palette className={`${width} ${height} ${color} mr-2`} />,
|
||||
Book: (color: string, width: string, height: string) => <Book className={`${width} ${height} ${color} mr-2`} />,
|
||||
Confetti: (color: string, width: string, height: string) => <Confetti className={`${width} ${height} ${color} mr-2`} />,
|
||||
House: (color: string, width: string, height: string) => <House className={`${width} ${height} ${color} mr-2`} />,
|
||||
Translate: (color: string, width: string, height: string) => <Translate className={`${width} ${height} ${color} mr-2`} />,
|
||||
Lightbulb: (color: string, width: string, height: string) => (
|
||||
<Lightbulb className={`${width} ${height} ${color} mr-2`} />
|
||||
),
|
||||
Robot: (color: string, width: string, height: string) => (
|
||||
<Robot className={`${width} ${height} ${color} mr-2`} />
|
||||
),
|
||||
Aperture: (color: string, width: string, height: string) => (
|
||||
<Aperture className={`${width} ${height} ${color} mr-2`} />
|
||||
),
|
||||
GraduationCap: (color: string, width: string, height: string) => (
|
||||
<GraduationCap className={`${width} ${height} ${color} mr-2`} />
|
||||
),
|
||||
Jeep: (color: string, width: string, height: string) => (
|
||||
<Jeep className={`${width} ${height} ${color} mr-2`} />
|
||||
),
|
||||
Island: (color: string, width: string, height: string) => (
|
||||
<Island className={`${width} ${height} ${color} mr-2`} />
|
||||
),
|
||||
MathOperations: (color: string, width: string, height: string) => (
|
||||
<MathOperations className={`${width} ${height} ${color} mr-2`} />
|
||||
),
|
||||
Asclepius: (color: string, width: string, height: string) => (
|
||||
<Asclepius className={`${width} ${height} ${color} mr-2`} />
|
||||
),
|
||||
Couch: (color: string, width: string, height: string) => (
|
||||
<Couch className={`${width} ${height} ${color} mr-2`} />
|
||||
),
|
||||
Code: (color: string, width: string, height: string) => (
|
||||
<Code className={`${width} ${height} ${color} mr-2`} />
|
||||
),
|
||||
Atom: (color: string, width: string, height: string) => (
|
||||
<Atom className={`${width} ${height} ${color} mr-2`} />
|
||||
),
|
||||
ClockCounterClockwise: (color: string, width: string, height: string) => (
|
||||
<ClockCounterClockwise className={`${width} ${height} ${color} mr-2`} />
|
||||
),
|
||||
Globe: (color: string, width: string, height: string) => (
|
||||
<Globe className={`${width} ${height} ${color} mr-2`} />
|
||||
),
|
||||
Palette: (color: string, width: string, height: string) => (
|
||||
<Palette className={`${width} ${height} ${color} mr-2`} />
|
||||
),
|
||||
Book: (color: string, width: string, height: string) => (
|
||||
<Book className={`${width} ${height} ${color} mr-2`} />
|
||||
),
|
||||
Confetti: (color: string, width: string, height: string) => (
|
||||
<Confetti className={`${width} ${height} ${color} mr-2`} />
|
||||
),
|
||||
House: (color: string, width: string, height: string) => (
|
||||
<House className={`${width} ${height} ${color} mr-2`} />
|
||||
),
|
||||
Translate: (color: string, width: string, height: string) => (
|
||||
<Translate className={`${width} ${height} ${color} mr-2`} />
|
||||
),
|
||||
};
|
||||
|
||||
function getIconFromIconName(iconName: string, color: string = 'gray', width: string = 'w-6', height: string = 'h-6') {
|
||||
function getIconFromIconName(
|
||||
iconName: string,
|
||||
color: string = "gray",
|
||||
width: string = "w-6",
|
||||
height: string = "h-6",
|
||||
) {
|
||||
const icon = iconMap[iconName];
|
||||
const colorName = color.toLowerCase();
|
||||
const colorClass = convertColorToTextClass(colorName);
|
||||
|
|
|
@ -11,12 +11,20 @@ export interface LocationData {
|
|||
timezone: string;
|
||||
}
|
||||
|
||||
const locationFetcher = () => window.fetch("https://ipapi.co/json").then((res) => res.json()).catch((err) => console.log(err));
|
||||
const locationFetcher = () =>
|
||||
window
|
||||
.fetch("https://ipapi.co/json")
|
||||
.then((res) => res.json())
|
||||
.catch((err) => console.log(err));
|
||||
|
||||
export const toTitleCase = (str: string) => str.replace(/\w\S*/g, (txt) => txt.charAt(0).toUpperCase() + txt.slice(1).toLowerCase());
|
||||
export const toTitleCase = (str: string) =>
|
||||
str.replace(/\w\S*/g, (txt) => txt.charAt(0).toUpperCase() + txt.slice(1).toLowerCase());
|
||||
|
||||
export function welcomeConsole() {
|
||||
console.log(`%c %s`, "font-family:monospace", `
|
||||
console.log(
|
||||
`%c %s`,
|
||||
"font-family:monospace",
|
||||
`
|
||||
__ __ __ __ ______ __ _____ __
|
||||
/\\ \\/ / /\\ \\_\\ \\ /\\ __ \\ /\\ \\ /\\ __ \\ /\\ \\
|
||||
\\ \\ _"-. \\ \\ __ \\ \\ \\ \\/\\ \\ _\\_\\ \\ \\ \\ __ \\ \\ \\ \\
|
||||
|
@ -30,11 +38,16 @@ export function welcomeConsole() {
|
|||
|
||||
See my source code at https://github.com/khoj-ai/khoj
|
||||
Read my operating manual at https://docs.khoj.dev
|
||||
`);
|
||||
`,
|
||||
);
|
||||
}
|
||||
|
||||
export function useIPLocationData() {
|
||||
const {data: locationData, error: locationDataError } = useSWR<LocationData>("/api/ip", locationFetcher, { revalidateOnFocus: false });
|
||||
const { data: locationData, error: locationDataError } = useSWR<LocationData>(
|
||||
"/api/ip",
|
||||
locationFetcher,
|
||||
{ revalidateOnFocus: false },
|
||||
);
|
||||
|
||||
if (locationDataError) return null;
|
||||
if (!locationData) return null;
|
||||
|
|
|
@ -1,20 +1,24 @@
|
|||
'use client'
|
||||
"use client";
|
||||
|
||||
import styles from './chatHistory.module.css';
|
||||
import { useRef, useEffect, useState } from 'react';
|
||||
import styles from "./chatHistory.module.css";
|
||||
import { useRef, useEffect, useState } from "react";
|
||||
|
||||
import ChatMessage, { ChatHistoryData, StreamMessage, TrainOfThought } from '../chatMessage/chatMessage';
|
||||
import ChatMessage, {
|
||||
ChatHistoryData,
|
||||
StreamMessage,
|
||||
TrainOfThought,
|
||||
} from "../chatMessage/chatMessage";
|
||||
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
|
||||
import { InlineLoading } from '../loading/loading';
|
||||
import { InlineLoading } from "../loading/loading";
|
||||
|
||||
import { Lightbulb } from "@phosphor-icons/react";
|
||||
|
||||
import ProfileCard from '../profileCard/profileCard';
|
||||
import { getIconFromIconName } from '@/app/common/iconUtils';
|
||||
import { AgentData } from '@/app/agents/page';
|
||||
import React from 'react';
|
||||
import ProfileCard from "../profileCard/profileCard";
|
||||
import { getIconFromIconName } from "@/app/common/iconUtils";
|
||||
import { AgentData } from "@/app/agents/page";
|
||||
import React from "react";
|
||||
|
||||
interface ChatResponse {
|
||||
status: string;
|
||||
|
@ -22,7 +26,7 @@ interface ChatResponse {
|
|||
}
|
||||
|
||||
interface ChatHistory {
|
||||
[key: string]: string
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
interface ChatHistoryProps {
|
||||
|
@ -34,21 +38,28 @@ interface ChatHistoryProps {
|
|||
setAgent: (agent: AgentData) => void;
|
||||
}
|
||||
|
||||
|
||||
function constructTrainOfThought(trainOfThought: string[], lastMessage: boolean, agentColor: string, key: string, completed: boolean = false) {
|
||||
function constructTrainOfThought(
|
||||
trainOfThought: string[],
|
||||
lastMessage: boolean,
|
||||
agentColor: string,
|
||||
key: string,
|
||||
completed: boolean = false,
|
||||
) {
|
||||
const lastIndex = trainOfThought.length - 1;
|
||||
return (
|
||||
<div className={`${styles.trainOfThought} shadow-sm`} key={key}>
|
||||
{
|
||||
!completed &&
|
||||
<InlineLoading className='float-right' />
|
||||
}
|
||||
{!completed && <InlineLoading className="float-right" />}
|
||||
|
||||
{trainOfThought.map((train, index) => (
|
||||
<TrainOfThought key={`train-${index}`} message={train} primary={index === lastIndex && lastMessage && !completed} agentColor={agentColor} />
|
||||
<TrainOfThought
|
||||
key={`train-${index}`}
|
||||
message={train}
|
||||
primary={index === lastIndex && lastMessage && !completed}
|
||||
agentColor={agentColor}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default function ChatHistory(props: ChatHistoryProps) {
|
||||
|
@ -60,19 +71,20 @@ export default function ChatHistory(props: ChatHistoryProps) {
|
|||
const chatHistoryRef = useRef<HTMLDivElement | null>(null);
|
||||
const sentinelRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const [incompleteIncomingMessageIndex, setIncompleteIncomingMessageIndex] = useState<number | null>(null);
|
||||
const [incompleteIncomingMessageIndex, setIncompleteIncomingMessageIndex] = useState<
|
||||
number | null
|
||||
>(null);
|
||||
const [fetchingData, setFetchingData] = useState(false);
|
||||
const [isMobileWidth, setIsMobileWidth] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('resize', () => {
|
||||
window.addEventListener("resize", () => {
|
||||
setIsMobileWidth(window.innerWidth < 768);
|
||||
});
|
||||
|
||||
setIsMobileWidth(window.innerWidth < 768);
|
||||
}, []);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
// This function ensures that scrolling to bottom happens after the data (chat messages) has been updated and rendered the first time.
|
||||
const scrollToBottomAfterDataLoad = () => {
|
||||
|
@ -88,20 +100,22 @@ export default function ChatHistory(props: ChatHistoryProps) {
|
|||
// Call the function defined above.
|
||||
scrollToBottomAfterDataLoad();
|
||||
}
|
||||
|
||||
}, [data, currentPage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasMoreMessages || fetchingData) return;
|
||||
|
||||
// TODO: A future optimization would be to add a time to delay to re-enabling the intersection observer.
|
||||
const observer = new IntersectionObserver(entries => {
|
||||
if (entries[0].isIntersecting && hasMoreMessages) {
|
||||
setFetchingData(true);
|
||||
fetchMoreMessages(currentPage);
|
||||
setCurrentPage((prev) => prev + 1);
|
||||
}
|
||||
}, { threshold: 1.0 });
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0].isIntersecting && hasMoreMessages) {
|
||||
setFetchingData(true);
|
||||
fetchMoreMessages(currentPage);
|
||||
setCurrentPage((prev) => prev + 1);
|
||||
}
|
||||
},
|
||||
{ threshold: 1.0 },
|
||||
);
|
||||
|
||||
if (sentinelRef.current) {
|
||||
observer.observe(sentinelRef.current);
|
||||
|
@ -128,14 +142,13 @@ export default function ChatHistory(props: ChatHistoryProps) {
|
|||
if (isUserAtBottom()) {
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
}, [props.incomingMessages]);
|
||||
|
||||
function fetchMoreMessages(currentPage: number) {
|
||||
if (!hasMoreMessages || fetchingData) return;
|
||||
const nextPage = currentPage + 1;
|
||||
|
||||
let conversationFetchURL = '';
|
||||
let conversationFetchURL = "";
|
||||
|
||||
if (props.conversationId) {
|
||||
conversationFetchURL = `/api/chat/history?client=web&conversation_id=${props.conversationId}&n=${10 * nextPage}`;
|
||||
|
@ -146,11 +159,15 @@ export default function ChatHistory(props: ChatHistoryProps) {
|
|||
}
|
||||
|
||||
fetch(conversationFetchURL)
|
||||
.then(response => response.json())
|
||||
.then((response) => response.json())
|
||||
.then((chatData: ChatResponse) => {
|
||||
props.setTitle(chatData.response.slug);
|
||||
if (chatData && chatData.response && chatData.response.chat && chatData.response.chat.length > 0) {
|
||||
|
||||
if (
|
||||
chatData &&
|
||||
chatData.response &&
|
||||
chatData.response.chat &&
|
||||
chatData.response.chat.length > 0
|
||||
) {
|
||||
if (chatData.response.chat.length === data?.chat.length) {
|
||||
setHasMoreMessages(false);
|
||||
setFetchingData(false);
|
||||
|
@ -171,7 +188,7 @@ export default function ChatHistory(props: ChatHistoryProps) {
|
|||
agent: chatData.response.agent,
|
||||
conversation_id: chatData.response.conversation_id,
|
||||
slug: chatData.response.slug,
|
||||
}
|
||||
};
|
||||
props.setAgent(chatData.response.agent);
|
||||
setData(chatMetadata);
|
||||
}
|
||||
|
@ -180,17 +197,17 @@ export default function ChatHistory(props: ChatHistoryProps) {
|
|||
setFetchingData(false);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
window.location.href = "/";
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
const scrollToBottom = () => {
|
||||
if (chatHistoryRef.current) {
|
||||
chatHistoryRef.current.scrollIntoView(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const isUserAtBottom = () => {
|
||||
if (!chatHistoryRef.current) return false;
|
||||
|
@ -202,11 +219,11 @@ export default function ChatHistory(props: ChatHistoryProps) {
|
|||
|
||||
// Considered at the bottom if within threshold pixels from the bottom
|
||||
return scrollTop + clientHeight >= scrollHeight - threshold;
|
||||
}
|
||||
};
|
||||
|
||||
function constructAgentLink() {
|
||||
if (!data || !data.agent || !data.agent.slug) return `/agents`;
|
||||
return `/agents?agent=${data.agent.slug}`
|
||||
return `/agents?agent=${data.agent.slug}`;
|
||||
}
|
||||
|
||||
function constructAgentName() {
|
||||
|
@ -226,104 +243,103 @@ export default function ChatHistory(props: ChatHistoryProps) {
|
|||
<ScrollArea className={`h-[80vh]`}>
|
||||
<div ref={ref}>
|
||||
<div className={styles.chatHistory} ref={chatHistoryRef}>
|
||||
<div ref={sentinelRef} style={{ height: '1px' }}>
|
||||
{fetchingData && <InlineLoading message="Loading Conversation" className='opacity-50' />}
|
||||
<div ref={sentinelRef} style={{ height: "1px" }}>
|
||||
{fetchingData && (
|
||||
<InlineLoading message="Loading Conversation" className="opacity-50" />
|
||||
)}
|
||||
</div>
|
||||
{(data && data.chat) && data.chat.map((chatMessage, index) => (
|
||||
<ChatMessage
|
||||
key={`${index}fullHistory`}
|
||||
isMobileWidth={isMobileWidth}
|
||||
chatMessage={chatMessage}
|
||||
customClassName='fullHistory'
|
||||
borderLeftColor={`${data?.agent.color}-500`}
|
||||
isLastMessage={index === data.chat.length - 1}
|
||||
/>
|
||||
))}
|
||||
{
|
||||
props.incomingMessages && props.incomingMessages.map((message, index) => {
|
||||
{data &&
|
||||
data.chat &&
|
||||
data.chat.map((chatMessage, index) => (
|
||||
<ChatMessage
|
||||
key={`${index}fullHistory`}
|
||||
isMobileWidth={isMobileWidth}
|
||||
chatMessage={chatMessage}
|
||||
customClassName="fullHistory"
|
||||
borderLeftColor={`${data?.agent.color}-500`}
|
||||
isLastMessage={index === data.chat.length - 1}
|
||||
/>
|
||||
))}
|
||||
{props.incomingMessages &&
|
||||
props.incomingMessages.map((message, index) => {
|
||||
return (
|
||||
<React.Fragment key={`incomingMessage${index}`}>
|
||||
|
||||
<ChatMessage
|
||||
key={`${index}outgoing`}
|
||||
isMobileWidth={isMobileWidth}
|
||||
chatMessage={
|
||||
{
|
||||
message: message.rawQuery,
|
||||
context: [],
|
||||
onlineContext: {},
|
||||
created: message.timestamp,
|
||||
by: "you",
|
||||
automationId: '',
|
||||
}
|
||||
}
|
||||
customClassName='fullHistory'
|
||||
chatMessage={{
|
||||
message: message.rawQuery,
|
||||
context: [],
|
||||
onlineContext: {},
|
||||
created: message.timestamp,
|
||||
by: "you",
|
||||
automationId: "",
|
||||
}}
|
||||
customClassName="fullHistory"
|
||||
borderLeftColor={`${data?.agent.color}-500`}
|
||||
/>
|
||||
{
|
||||
message.trainOfThought &&
|
||||
{message.trainOfThought &&
|
||||
constructTrainOfThought(
|
||||
message.trainOfThought,
|
||||
index === incompleteIncomingMessageIndex,
|
||||
data?.agent.color || 'orange',
|
||||
`${index}trainOfThought`, message.completed)
|
||||
}
|
||||
data?.agent.color || "orange",
|
||||
`${index}trainOfThought`,
|
||||
message.completed,
|
||||
)}
|
||||
<ChatMessage
|
||||
key={`${index}incoming`}
|
||||
isMobileWidth={isMobileWidth}
|
||||
chatMessage={
|
||||
{
|
||||
message: message.rawResponse,
|
||||
context: message.context,
|
||||
onlineContext: message.onlineContext,
|
||||
created: message.timestamp,
|
||||
by: "khoj",
|
||||
automationId: '',
|
||||
rawQuery: message.rawQuery,
|
||||
}
|
||||
}
|
||||
customClassName='fullHistory'
|
||||
chatMessage={{
|
||||
message: message.rawResponse,
|
||||
context: message.context,
|
||||
onlineContext: message.onlineContext,
|
||||
created: message.timestamp,
|
||||
by: "khoj",
|
||||
automationId: "",
|
||||
rawQuery: message.rawQuery,
|
||||
}}
|
||||
customClassName="fullHistory"
|
||||
borderLeftColor={`${data?.agent.color}-500`}
|
||||
isLastMessage={true}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)
|
||||
})
|
||||
}
|
||||
{
|
||||
props.pendingMessage &&
|
||||
);
|
||||
})}
|
||||
{props.pendingMessage && (
|
||||
<ChatMessage
|
||||
key={`pendingMessage-${props.pendingMessage.length}`}
|
||||
isMobileWidth={isMobileWidth}
|
||||
chatMessage={
|
||||
{
|
||||
message: props.pendingMessage,
|
||||
context: [],
|
||||
onlineContext: {},
|
||||
created: (new Date().getTime()).toString(),
|
||||
by: "you",
|
||||
automationId: '',
|
||||
}
|
||||
}
|
||||
customClassName='fullHistory'
|
||||
chatMessage={{
|
||||
message: props.pendingMessage,
|
||||
context: [],
|
||||
onlineContext: {},
|
||||
created: new Date().getTime().toString(),
|
||||
by: "you",
|
||||
automationId: "",
|
||||
}}
|
||||
customClassName="fullHistory"
|
||||
borderLeftColor={`${data?.agent.color}-500`}
|
||||
isLastMessage={true}
|
||||
/>
|
||||
}
|
||||
{data &&
|
||||
)}
|
||||
{data && (
|
||||
<div className={`${styles.agentIndicator} pb-4`}>
|
||||
<div className="relative group mx-2 cursor-pointer">
|
||||
<ProfileCard
|
||||
name={constructAgentName()}
|
||||
link={constructAgentLink()}
|
||||
avatar={getIconFromIconName(data.agent.icon, data.agent.color) || <Lightbulb />}
|
||||
avatar={
|
||||
getIconFromIconName(data.agent.icon, data.agent.color) || (
|
||||
<Lightbulb />
|
||||
)
|
||||
}
|
||||
description={constructAgentPersona()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import styles from "./chatInputArea.module.css";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
|
||||
import styles from './chatInputArea.module.css';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { uploadDataForIndexing } from "../../common/chatFunctions";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
|
||||
import { uploadDataForIndexing } from '../../common/chatFunctions';
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
|
||||
import 'katex/dist/katex.min.css';
|
||||
import "katex/dist/katex.min.css";
|
||||
import {
|
||||
ArrowRight,
|
||||
ArrowUp,
|
||||
|
@ -21,7 +20,7 @@ import {
|
|||
Robot,
|
||||
Shapes,
|
||||
Stop,
|
||||
} from '@phosphor-icons/react';
|
||||
} from "@phosphor-icons/react";
|
||||
|
||||
import {
|
||||
Command,
|
||||
|
@ -31,27 +30,27 @@ import {
|
|||
CommandItem,
|
||||
CommandList,
|
||||
CommandSeparator,
|
||||
} from "@/components/ui/command"
|
||||
} from "@/components/ui/command";
|
||||
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { Popover, PopoverContent } from '@/components/ui/popover';
|
||||
import { PopoverTrigger } from '@radix-ui/react-popover';
|
||||
import LoginPrompt from '../loginPrompt/loginPrompt';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { InlineLoading } from '../loading/loading';
|
||||
import { convertToBGClass } from '@/app/common/colorUtils';
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Popover, PopoverContent } from "@/components/ui/popover";
|
||||
import { PopoverTrigger } from "@radix-ui/react-popover";
|
||||
import LoginPrompt from "../loginPrompt/loginPrompt";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { InlineLoading } from "../loading/loading";
|
||||
import { convertToBGClass } from "@/app/common/colorUtils";
|
||||
|
||||
export interface ChatOptions {
|
||||
[key: string]: string
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
interface ChatInputProps {
|
||||
|
@ -66,7 +65,7 @@ interface ChatInputProps {
|
|||
}
|
||||
|
||||
export default function ChatInputArea(props: ChatInputProps) {
|
||||
const [message, setMessage] = useState('');
|
||||
const [message, setMessage] = useState("");
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const [warning, setWarning] = useState<string | null>(null);
|
||||
|
@ -101,13 +100,15 @@ export default function ChatInputArea(props: ChatInputProps) {
|
|||
if (!message.trim()) return;
|
||||
|
||||
if (!props.isLoggedIn) {
|
||||
setLoginRedirectMessage('Hey there, you need to be signed in to send messages to Khoj AI');
|
||||
setLoginRedirectMessage(
|
||||
"Hey there, you need to be signed in to send messages to Khoj AI",
|
||||
);
|
||||
setShowLoginPrompt(true);
|
||||
return;
|
||||
}
|
||||
|
||||
props.sendMessage(message.trim());
|
||||
setMessage('');
|
||||
setMessage("");
|
||||
}
|
||||
|
||||
function handleSlashCommandClick(command: string) {
|
||||
|
@ -123,7 +124,7 @@ export default function ChatInputArea(props: ChatInputProps) {
|
|||
if (!event.target.files) return;
|
||||
|
||||
if (!props.isLoggedIn) {
|
||||
setLoginRedirectMessage('Whoa! You need to login to upload files');
|
||||
setLoginRedirectMessage("Whoa! You need to login to upload files");
|
||||
setShowLoginPrompt(true);
|
||||
return;
|
||||
}
|
||||
|
@ -134,78 +135,79 @@ export default function ChatInputArea(props: ChatInputProps) {
|
|||
setUploading,
|
||||
setError,
|
||||
props.setUploadedFiles,
|
||||
props.conversationId);
|
||||
props.conversationId,
|
||||
);
|
||||
}
|
||||
|
||||
function getIconForSlashCommand(command: string) {
|
||||
const className = 'h-4 w-4 mr-2';
|
||||
if (command.includes('summarize')) {
|
||||
return <Gps className={className} />
|
||||
const className = "h-4 w-4 mr-2";
|
||||
if (command.includes("summarize")) {
|
||||
return <Gps className={className} />;
|
||||
}
|
||||
|
||||
if (command.includes('help')) {
|
||||
return <Question className={className} />
|
||||
if (command.includes("help")) {
|
||||
return <Question className={className} />;
|
||||
}
|
||||
|
||||
if (command.includes('automation')) {
|
||||
return <Robot className={className} />
|
||||
if (command.includes("automation")) {
|
||||
return <Robot className={className} />;
|
||||
}
|
||||
|
||||
if (command.includes('webpage')) {
|
||||
return <Browser className={className} />
|
||||
if (command.includes("webpage")) {
|
||||
return <Browser className={className} />;
|
||||
}
|
||||
|
||||
if (command.includes('notes')) {
|
||||
return <Notebook className={className} />
|
||||
if (command.includes("notes")) {
|
||||
return <Notebook className={className} />;
|
||||
}
|
||||
|
||||
if (command.includes('image')) {
|
||||
return <Image className={className} />
|
||||
if (command.includes("image")) {
|
||||
return <Image className={className} />;
|
||||
}
|
||||
|
||||
if (command.includes('default')) {
|
||||
return <Shapes className={className} />
|
||||
if (command.includes("default")) {
|
||||
return <Shapes className={className} />;
|
||||
}
|
||||
|
||||
if (command.includes('general')) {
|
||||
return <ChatsTeardrop className={className} />
|
||||
if (command.includes("general")) {
|
||||
return <ChatsTeardrop className={className} />;
|
||||
}
|
||||
|
||||
if (command.includes('online')) {
|
||||
return <GlobeSimple className={className} />
|
||||
if (command.includes("online")) {
|
||||
return <GlobeSimple className={className} />;
|
||||
}
|
||||
return <ArrowRight className={className} />
|
||||
return <ArrowRight className={className} />;
|
||||
}
|
||||
|
||||
// Assuming this function is added within the same context as the provided excerpt
|
||||
async function startRecordingAndTranscribe() {
|
||||
try {
|
||||
const microphone = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
const mediaRecorder = new MediaRecorder(microphone, { mimeType: 'audio/webm' });
|
||||
const mediaRecorder = new MediaRecorder(microphone, { mimeType: "audio/webm" });
|
||||
|
||||
const audioChunks: Blob[] = [];
|
||||
|
||||
mediaRecorder.ondataavailable = async (event) => {
|
||||
audioChunks.push(event.data);
|
||||
const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
|
||||
const audioBlob = new Blob(audioChunks, { type: "audio/webm" });
|
||||
const formData = new FormData();
|
||||
formData.append('file', audioBlob);
|
||||
formData.append("file", audioBlob);
|
||||
|
||||
// Send the incremental audio blob to the server
|
||||
try {
|
||||
const response = await fetch('/api/transcribe', {
|
||||
method: 'POST',
|
||||
const response = await fetch("/api/transcribe", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
|
||||
const transcription = await response.json();
|
||||
setMessage(transcription.text.trim());
|
||||
} catch (error) {
|
||||
console.error('Error sending audio to server:', error);
|
||||
console.error("Error sending audio to server:", error);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -213,27 +215,27 @@ export default function ChatInputArea(props: ChatInputProps) {
|
|||
mediaRecorder.start(1500);
|
||||
|
||||
mediaRecorder.onstop = async () => {
|
||||
const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
|
||||
const audioBlob = new Blob(audioChunks, { type: "audio/webm" });
|
||||
const formData = new FormData();
|
||||
formData.append('file', audioBlob);
|
||||
formData.append("file", audioBlob);
|
||||
|
||||
// Send the audio blob to the server
|
||||
try {
|
||||
const response = await fetch('/api/transcribe', {
|
||||
method: 'POST',
|
||||
const response = await fetch("/api/transcribe", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
|
||||
const transcription = await response.json();
|
||||
mediaRecorder.stream.getTracks().forEach(track => track.stop());
|
||||
mediaRecorder.stream.getTracks().forEach((track) => track.stop());
|
||||
setMediaRecorder(null);
|
||||
setMessage(transcription.text.trim());
|
||||
} catch (error) {
|
||||
console.error('Error sending audio to server:', error);
|
||||
console.error("Error sending audio to server:", error);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -251,106 +253,117 @@ export default function ChatInputArea(props: ChatInputProps) {
|
|||
if (recording && !mediaRecorder) {
|
||||
startRecordingAndTranscribe();
|
||||
}
|
||||
|
||||
}, [recording, mediaRecorder]);
|
||||
|
||||
const chatInputRef = useRef<HTMLTextAreaElement>(null);
|
||||
useEffect(() => {
|
||||
if (!chatInputRef.current) return;
|
||||
chatInputRef.current.style.height = 'auto';
|
||||
chatInputRef.current.style.height = Math.max(chatInputRef.current.scrollHeight - 24, 64) + 'px';
|
||||
chatInputRef.current.style.height = "auto";
|
||||
chatInputRef.current.style.height =
|
||||
Math.max(chatInputRef.current.scrollHeight - 24, 64) + "px";
|
||||
}, [message]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
showLoginPrompt && loginRedirectMessage && (
|
||||
<LoginPrompt
|
||||
onOpenChange={setShowLoginPrompt}
|
||||
loginRedirectMessage={loginRedirectMessage} />
|
||||
)
|
||||
}
|
||||
{
|
||||
uploading && (
|
||||
<AlertDialog
|
||||
open={uploading}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Uploading data. Please wait.</AlertDialogTitle>
|
||||
</AlertDialogHeader>
|
||||
{showLoginPrompt && loginRedirectMessage && (
|
||||
<LoginPrompt
|
||||
onOpenChange={setShowLoginPrompt}
|
||||
loginRedirectMessage={loginRedirectMessage}
|
||||
/>
|
||||
)}
|
||||
{uploading && (
|
||||
<AlertDialog open={uploading}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Uploading data. Please wait.</AlertDialogTitle>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogDescription>
|
||||
<Progress
|
||||
indicatorColor="bg-slate-500"
|
||||
className="w-full h-2 rounded-full"
|
||||
value={progressValue}
|
||||
/>
|
||||
</AlertDialogDescription>
|
||||
<AlertDialogAction
|
||||
className="bg-slate-400 hover:bg-slate-500"
|
||||
onClick={() => setUploading(false)}
|
||||
>
|
||||
Dismiss
|
||||
</AlertDialogAction>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)}
|
||||
{warning && (
|
||||
<AlertDialog open={warning !== null}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Data Upload Warning</AlertDialogTitle>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogDescription>{warning}</AlertDialogDescription>
|
||||
<AlertDialogAction
|
||||
className="bg-slate-400 hover:bg-slate-500"
|
||||
onClick={() => setWarning(null)}
|
||||
>
|
||||
Close
|
||||
</AlertDialogAction>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)}
|
||||
{error && (
|
||||
<AlertDialog open={error !== null}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Oh no!</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
<Progress
|
||||
indicatorColor='bg-slate-500'
|
||||
className='w-full h-2 rounded-full'
|
||||
value={progressValue} />
|
||||
Something went wrong while uploading your data
|
||||
</AlertDialogDescription>
|
||||
<AlertDialogAction className='bg-slate-400 hover:bg-slate-500' onClick={() => setUploading(false)}>Dismiss</AlertDialogAction>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)}
|
||||
{
|
||||
warning && (
|
||||
<AlertDialog
|
||||
open={warning !== null}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Data Upload Warning</AlertDialogTitle>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogDescription>{warning}</AlertDialogDescription>
|
||||
<AlertDialogAction className='bg-slate-400 hover:bg-slate-500' onClick={() => setWarning(null)}>Close</AlertDialogAction>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)
|
||||
}
|
||||
{
|
||||
error && (
|
||||
<AlertDialog
|
||||
open={error !== null}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Oh no!</AlertDialogTitle>
|
||||
<AlertDialogDescription>Something went wrong while uploading your data</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogDescription>{error}</AlertDialogDescription>
|
||||
<AlertDialogAction className='bg-slate-400 hover:bg-slate-500' onClick={() => setError(null)}>Close</AlertDialogAction>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)
|
||||
}
|
||||
{
|
||||
(message.startsWith('/') && message.split(' ').length === 1) &&
|
||||
<div className='flex justify-center text-center'>
|
||||
<Popover
|
||||
open={message.startsWith('/')}>
|
||||
<PopoverTrigger className='flex justify-center text-center'>
|
||||
|
||||
</PopoverTrigger>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogDescription>{error}</AlertDialogDescription>
|
||||
<AlertDialogAction
|
||||
className="bg-slate-400 hover:bg-slate-500"
|
||||
onClick={() => setError(null)}
|
||||
>
|
||||
Close
|
||||
</AlertDialogAction>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)}
|
||||
{message.startsWith("/") && message.split(" ").length === 1 && (
|
||||
<div className="flex justify-center text-center">
|
||||
<Popover open={message.startsWith("/")}>
|
||||
<PopoverTrigger className="flex justify-center text-center"></PopoverTrigger>
|
||||
<PopoverContent
|
||||
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||
className={`${props.isMobileWidth ? 'w-[100vw]' : 'w-full'} rounded-md`}>
|
||||
<Command className='max-w-full'>
|
||||
<CommandInput placeholder="Type a command or search..." value={message} className='hidden' />
|
||||
className={`${props.isMobileWidth ? "w-[100vw]" : "w-full"} rounded-md`}
|
||||
>
|
||||
<Command className="max-w-full">
|
||||
<CommandInput
|
||||
placeholder="Type a command or search..."
|
||||
value={message}
|
||||
className="hidden"
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>No matching commands.</CommandEmpty>
|
||||
<CommandGroup heading="Agent Tools">
|
||||
{props.chatOptionsData && Object.entries(props.chatOptionsData).map(([key, value]) => (
|
||||
<CommandItem
|
||||
key={key}
|
||||
className={`text-md`}
|
||||
onSelect={() => handleSlashCommandClick(key)}>
|
||||
<div
|
||||
className='grid grid-cols-1 gap-1'>
|
||||
<div
|
||||
className='font-bold flex items-center'>
|
||||
{getIconForSlashCommand(key)}
|
||||
/{key}
|
||||
</div>
|
||||
<div>
|
||||
{value}
|
||||
</div>
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
{props.chatOptionsData &&
|
||||
Object.entries(props.chatOptionsData).map(
|
||||
([key, value]) => (
|
||||
<CommandItem
|
||||
key={key}
|
||||
className={`text-md`}
|
||||
onSelect={() =>
|
||||
handleSlashCommandClick(key)
|
||||
}
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-1">
|
||||
<div className="font-bold flex items-center">
|
||||
{getIconForSlashCommand(key)}/{key}
|
||||
</div>
|
||||
<div>{value}</div>
|
||||
</div>
|
||||
</CommandItem>
|
||||
),
|
||||
)}
|
||||
</CommandGroup>
|
||||
<CommandSeparator />
|
||||
</CommandList>
|
||||
|
@ -358,94 +371,95 @@ export default function ChatInputArea(props: ChatInputProps) {
|
|||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
}
|
||||
<div className={`${styles.actualInputArea} items-center justify-between dark:bg-neutral-700`}>
|
||||
)}
|
||||
<div
|
||||
className={`${styles.actualInputArea} items-center justify-between dark:bg-neutral-700`}
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
multiple={true}
|
||||
ref={fileInputRef}
|
||||
onChange={handleFileChange}
|
||||
style={{ display: 'none' }}
|
||||
style={{ display: "none" }}
|
||||
/>
|
||||
<Button
|
||||
variant={'ghost'}
|
||||
variant={"ghost"}
|
||||
className="!bg-none p-0 m-2 h-auto text-3xl rounded-full text-gray-300 hover:text-gray-500"
|
||||
disabled={props.sendDisabled}
|
||||
onClick={handleFileButtonClick}>
|
||||
<Paperclip className='w-8 h-8' />
|
||||
onClick={handleFileButtonClick}
|
||||
>
|
||||
<Paperclip className="w-8 h-8" />
|
||||
</Button>
|
||||
<div className="grid w-full gap-1.5 relative">
|
||||
<Textarea
|
||||
ref={chatInputRef}
|
||||
className={`border-none w-full h-16 min-h-16 max-h-[128px] md:py-4 rounded-lg resize-none dark:bg-neutral-700 ${props.isMobileWidth ? 'text-md' : 'text-lg'}`}
|
||||
className={`border-none w-full h-16 min-h-16 max-h-[128px] md:py-4 rounded-lg resize-none dark:bg-neutral-700 ${props.isMobileWidth ? "text-md" : "text-lg"}`}
|
||||
placeholder="Type / to see a list of commands"
|
||||
id="message"
|
||||
autoFocus={true}
|
||||
value={message}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
onSendMessage();
|
||||
}
|
||||
}}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
disabled={props.sendDisabled || recording} />
|
||||
disabled={props.sendDisabled || recording}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
recording ?
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant='default'
|
||||
className={`${!recording && 'hidden'} ${props.agentColor ? convertToBGClass(props.agentColor) : 'bg-orange-300 hover:bg-orange-500'} rounded-full p-1 m-2 h-auto text-3xl transition transform md:hover:-translate-y-1`}
|
||||
onClick={() => {
|
||||
setRecording(!recording);
|
||||
}}
|
||||
disabled={props.sendDisabled}
|
||||
>
|
||||
<Stop weight='fill' className='w-6 h-6' />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Click to stop recording and transcribe your voice.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
:
|
||||
(
|
||||
mediaRecorder ?
|
||||
<InlineLoading />
|
||||
:
|
||||
< TooltipProvider >
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant='default'
|
||||
className={`${(!message || recording) || 'hidden'} ${props.agentColor ? convertToBGClass(props.agentColor) : 'bg-orange-300 hover:bg-orange-500'} rounded-full p-1 m-2 h-auto text-3xl transition transform md:hover:-translate-y-1`}
|
||||
onClick={() => {
|
||||
setMessage("Listening...");
|
||||
setRecording(!recording);
|
||||
}}
|
||||
disabled={props.sendDisabled}
|
||||
>
|
||||
<Microphone weight='fill' className='w-6 h-6' />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Click to transcribe your message with voice.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
{recording ? (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="default"
|
||||
className={`${!recording && "hidden"} ${props.agentColor ? convertToBGClass(props.agentColor) : "bg-orange-300 hover:bg-orange-500"} rounded-full p-1 m-2 h-auto text-3xl transition transform md:hover:-translate-y-1`}
|
||||
onClick={() => {
|
||||
setRecording(!recording);
|
||||
}}
|
||||
disabled={props.sendDisabled}
|
||||
>
|
||||
<Stop weight="fill" className="w-6 h-6" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Click to stop recording and transcribe your voice.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
) : mediaRecorder ? (
|
||||
<InlineLoading />
|
||||
) : (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="default"
|
||||
className={`${!message || recording || "hidden"} ${props.agentColor ? convertToBGClass(props.agentColor) : "bg-orange-300 hover:bg-orange-500"} rounded-full p-1 m-2 h-auto text-3xl transition transform md:hover:-translate-y-1`}
|
||||
onClick={() => {
|
||||
setMessage("Listening...");
|
||||
setRecording(!recording);
|
||||
}}
|
||||
disabled={props.sendDisabled}
|
||||
>
|
||||
<Microphone weight="fill" className="w-6 h-6" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Click to transcribe your message with voice.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
<Button
|
||||
className={`${(!message || recording) && 'hidden'} ${props.agentColor ? convertToBGClass(props.agentColor) : 'bg-orange-300 hover:bg-orange-500'} rounded-full p-1 m-2 h-auto text-3xl transition transform md:hover:-translate-y-1`}
|
||||
className={`${(!message || recording) && "hidden"} ${props.agentColor ? convertToBGClass(props.agentColor) : "bg-orange-300 hover:bg-orange-500"} rounded-full p-1 m-2 h-auto text-3xl transition transform md:hover:-translate-y-1`}
|
||||
onClick={onSendMessage}
|
||||
disabled={props.sendDisabled}>
|
||||
<ArrowUp className='w-6 h-6' weight='bold' />
|
||||
disabled={props.sendDisabled}
|
||||
>
|
||||
<ArrowUp className="w-6 h-6" weight="bold" />
|
||||
</Button>
|
||||
</div >
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,34 +1,47 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import styles from './chatMessage.module.css';
|
||||
import styles from "./chatMessage.module.css";
|
||||
|
||||
import markdownIt from 'markdown-it';
|
||||
import markdownIt from "markdown-it";
|
||||
import mditHljs from "markdown-it-highlightjs";
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
|
||||
import 'katex/dist/katex.min.css';
|
||||
import "katex/dist/katex.min.css";
|
||||
|
||||
import { TeaserReferencesSection, constructAllReferences } from '../referencePanel/referencePanel';
|
||||
import { TeaserReferencesSection, constructAllReferences } from "../referencePanel/referencePanel";
|
||||
|
||||
import { ThumbsUp, ThumbsDown, Copy, Brain, Cloud, Folder, Book, Aperture, SpeakerHigh, MagnifyingGlass, Pause, Palette } from '@phosphor-icons/react';
|
||||
import {
|
||||
ThumbsUp,
|
||||
ThumbsDown,
|
||||
Copy,
|
||||
Brain,
|
||||
Cloud,
|
||||
Folder,
|
||||
Book,
|
||||
Aperture,
|
||||
SpeakerHigh,
|
||||
MagnifyingGlass,
|
||||
Pause,
|
||||
Palette,
|
||||
} from "@phosphor-icons/react";
|
||||
|
||||
import DOMPurify from 'dompurify';
|
||||
import { InlineLoading } from '../loading/loading';
|
||||
import { convertColorToTextClass } from '@/app/common/colorUtils';
|
||||
import { AgentData } from '@/app/agents/page';
|
||||
import DOMPurify from "dompurify";
|
||||
import { InlineLoading } from "../loading/loading";
|
||||
import { convertColorToTextClass } from "@/app/common/colorUtils";
|
||||
import { AgentData } from "@/app/agents/page";
|
||||
|
||||
import renderMathInElement from 'katex/contrib/auto-render';
|
||||
import 'katex/dist/katex.min.css';
|
||||
import renderMathInElement from "katex/contrib/auto-render";
|
||||
import "katex/dist/katex.min.css";
|
||||
|
||||
const md = new markdownIt({
|
||||
html: true,
|
||||
linkify: true,
|
||||
typographer: true
|
||||
typographer: true,
|
||||
});
|
||||
|
||||
md.use(mditHljs, {
|
||||
inline: true,
|
||||
code: true
|
||||
code: true,
|
||||
});
|
||||
|
||||
export interface Context {
|
||||
|
@ -65,18 +78,18 @@ export interface OnlineContextData {
|
|||
answer: string;
|
||||
source: string;
|
||||
title: string;
|
||||
}
|
||||
};
|
||||
knowledgeGraph: {
|
||||
attributes: {
|
||||
[key: string]: string;
|
||||
}
|
||||
};
|
||||
description: string;
|
||||
descriptionLink: string;
|
||||
descriptionSource: string;
|
||||
imageUrl: string;
|
||||
title: string;
|
||||
type: string;
|
||||
}
|
||||
};
|
||||
organic: OrganicContext[];
|
||||
peopleAlsoAsk: PeopleAlsoAsk[];
|
||||
}
|
||||
|
@ -119,26 +132,34 @@ export interface ChatHistoryData {
|
|||
}
|
||||
|
||||
function sendFeedback(uquery: string, kquery: string, sentiment: string) {
|
||||
fetch('/api/chat/feedback', {
|
||||
method: 'POST',
|
||||
fetch("/api/chat/feedback", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ uquery: uquery, kquery: kquery, sentiment: sentiment })
|
||||
})
|
||||
body: JSON.stringify({ uquery: uquery, kquery: kquery, sentiment: sentiment }),
|
||||
});
|
||||
}
|
||||
|
||||
function FeedbackButtons({ uquery, kquery }: { uquery: string, kquery: string }) {
|
||||
function FeedbackButtons({ uquery, kquery }: { uquery: string; kquery: string }) {
|
||||
return (
|
||||
<div className={`${styles.feedbackButtons} flex align-middle justify-center items-center`}>
|
||||
<button title="Like" className={styles.thumbsUpButton} onClick={() => sendFeedback(uquery, kquery, 'positive')}>
|
||||
<ThumbsUp alt="Like Message" color='hsl(var(--muted-foreground))' />
|
||||
<button
|
||||
title="Like"
|
||||
className={styles.thumbsUpButton}
|
||||
onClick={() => sendFeedback(uquery, kquery, "positive")}
|
||||
>
|
||||
<ThumbsUp alt="Like Message" color="hsl(var(--muted-foreground))" />
|
||||
</button>
|
||||
<button title="Dislike" className={styles.thumbsDownButton} onClick={() => sendFeedback(uquery, kquery, 'negative')}>
|
||||
<ThumbsDown alt="Dislike Message" color='hsl(var(--muted-foreground))' />
|
||||
<button
|
||||
title="Dislike"
|
||||
className={styles.thumbsDownButton}
|
||||
onClick={() => sendFeedback(uquery, kquery, "negative")}
|
||||
>
|
||||
<ThumbsDown alt="Dislike Message" color="hsl(var(--muted-foreground))" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
interface ChatMessageProps {
|
||||
|
@ -160,7 +181,7 @@ function chooseIconFromHeader(header: string, iconColor: string) {
|
|||
const compareHeader = header.toLowerCase();
|
||||
const classNames = `inline mt-1 mr-2 ${iconColor} h-4 w-4`;
|
||||
if (compareHeader.includes("understanding")) {
|
||||
return <Brain className={`${classNames}`} />
|
||||
return <Brain className={`${classNames}`} />;
|
||||
}
|
||||
|
||||
if (compareHeader.includes("generating")) {
|
||||
|
@ -183,7 +204,11 @@ function chooseIconFromHeader(header: string, iconColor: string) {
|
|||
return <MagnifyingGlass className={`${classNames}`} />;
|
||||
}
|
||||
|
||||
if (compareHeader.includes("summary") || compareHeader.includes("summarize") || compareHeader.includes("enhanc")) {
|
||||
if (
|
||||
compareHeader.includes("summary") ||
|
||||
compareHeader.includes("summarize") ||
|
||||
compareHeader.includes("enhanc")
|
||||
) {
|
||||
return <Aperture className={`${classNames}`} />;
|
||||
}
|
||||
|
||||
|
@ -198,21 +223,23 @@ export function TrainOfThought(props: TrainOfThoughtProps) {
|
|||
// The train of thought comes in as a markdown-formatted string. It starts with a heading delimited by two asterisks at the start and end and a colon, followed by the message. Example: **header**: status. This function will parse the message and render it as a div.
|
||||
let extractedHeader = props.message.match(/\*\*(.*)\*\*/);
|
||||
let header = extractedHeader ? extractedHeader[1] : "";
|
||||
const iconColor = props.primary ? convertColorToTextClass(props.agentColor) : 'text-gray-500';
|
||||
const iconColor = props.primary ? convertColorToTextClass(props.agentColor) : "text-gray-500";
|
||||
const icon = chooseIconFromHeader(header, iconColor);
|
||||
let markdownRendered = DOMPurify.sanitize(md.render(props.message));
|
||||
return (
|
||||
<div className={`${styles.trainOfThoughtElement} items-center ${props.primary ? 'text-gray-400' : 'text-gray-300'} ${styles.trainOfThought} ${props.primary ? styles.primary : ''}`} >
|
||||
<div
|
||||
className={`${styles.trainOfThoughtElement} items-center ${props.primary ? "text-gray-400" : "text-gray-300"} ${styles.trainOfThought} ${props.primary ? styles.primary : ""}`}
|
||||
>
|
||||
{icon}
|
||||
<div dangerouslySetInnerHTML={{ __html: markdownRendered }} />
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default function ChatMessage(props: ChatMessageProps) {
|
||||
const [copySuccess, setCopySuccess] = useState<boolean>(false);
|
||||
const [isHovering, setIsHovering] = useState<boolean>(false);
|
||||
const [markdownRendered, setMarkdownRendered] = useState<string>('');
|
||||
const [markdownRendered, setMarkdownRendered] = useState<string>("");
|
||||
const [isPlaying, setIsPlaying] = useState<boolean>(false);
|
||||
const [interrupted, setInterrupted] = useState<boolean>(false);
|
||||
|
||||
|
@ -229,17 +256,17 @@ export default function ChatMessage(props: ChatMessageProps) {
|
|||
// If the addedNodes property has one or more nodes
|
||||
if (messageRef.current) {
|
||||
for (let mutation of mutationsList) {
|
||||
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
|
||||
if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
|
||||
// Call your function here
|
||||
|
||||
console.log("render katex in body");
|
||||
|
||||
renderMathInElement(messageRef.current, {
|
||||
delimiters: [
|
||||
{ left: '$$', right: '$$', display: true },
|
||||
{ left: '\\[', right: '\\]', display: true },
|
||||
{ left: '$', right: '$', display: false },
|
||||
{ left: '\\(', right: '\\)', display: false },
|
||||
{ left: "$$", right: "$$", display: true },
|
||||
{ left: "\\[", right: "\\]", display: true },
|
||||
{ left: "$", right: "$", display: false },
|
||||
{ left: "\\(", right: "\\)", display: false },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
@ -259,25 +286,38 @@ export default function ChatMessage(props: ChatMessageProps) {
|
|||
let message = props.chatMessage.message;
|
||||
|
||||
// Replace LaTeX delimiters with placeholders
|
||||
message = message.replace(/\\\(/g, 'LEFTPAREN').replace(/\\\)/g, 'RIGHTPAREN')
|
||||
.replace(/\\\[/g, 'LEFTBRACKET').replace(/\\\]/g, 'RIGHTBRACKET');
|
||||
message = message
|
||||
.replace(/\\\(/g, "LEFTPAREN")
|
||||
.replace(/\\\)/g, "RIGHTPAREN")
|
||||
.replace(/\\\[/g, "LEFTBRACKET")
|
||||
.replace(/\\\]/g, "RIGHTBRACKET");
|
||||
|
||||
if (props.chatMessage.intent && props.chatMessage.intent.type == "text-to-image") {
|
||||
message = `![generated image](data:image/png;base64,${message})`;
|
||||
} else if (props.chatMessage.intent && props.chatMessage.intent.type == "text-to-image2") {
|
||||
message = `![generated image](${message})`;
|
||||
} else if (props.chatMessage.intent && props.chatMessage.intent.type == "text-to-image-v3") {
|
||||
} else if (
|
||||
props.chatMessage.intent &&
|
||||
props.chatMessage.intent.type == "text-to-image-v3"
|
||||
) {
|
||||
message = `![generated image](data:image/webp;base64,${message})`;
|
||||
}
|
||||
if (props.chatMessage.intent && props.chatMessage.intent.type.includes("text-to-image") && props.chatMessage.intent["inferred-queries"]?.length > 0) {
|
||||
if (
|
||||
props.chatMessage.intent &&
|
||||
props.chatMessage.intent.type.includes("text-to-image") &&
|
||||
props.chatMessage.intent["inferred-queries"]?.length > 0
|
||||
) {
|
||||
message += `\n\n**Inferred Query**\n\n${props.chatMessage.intent["inferred-queries"][0]}`;
|
||||
}
|
||||
|
||||
let markdownRendered = md.render(message);
|
||||
|
||||
// Replace placeholders with LaTeX delimiters
|
||||
markdownRendered = markdownRendered.replace(/LEFTPAREN/g, '\\(').replace(/RIGHTPAREN/g, '\\)')
|
||||
.replace(/LEFTBRACKET/g, '\\[').replace(/RIGHTBRACKET/g, '\\]');
|
||||
markdownRendered = markdownRendered
|
||||
.replace(/LEFTPAREN/g, "\\(")
|
||||
.replace(/RIGHTPAREN/g, "\\)")
|
||||
.replace(/LEFTBRACKET/g, "\\[")
|
||||
.replace(/RIGHTBRACKET/g, "\\]");
|
||||
|
||||
// Sanitize and set the rendered markdown
|
||||
setMarkdownRendered(DOMPurify.sanitize(markdownRendered));
|
||||
|
@ -293,25 +333,25 @@ export default function ChatMessage(props: ChatMessageProps) {
|
|||
|
||||
useEffect(() => {
|
||||
if (messageRef.current) {
|
||||
const preElements = messageRef.current.querySelectorAll('pre > .hljs');
|
||||
const preElements = messageRef.current.querySelectorAll("pre > .hljs");
|
||||
preElements.forEach((preElement) => {
|
||||
const copyButton = document.createElement('button');
|
||||
const copyImage = document.createElement('img');
|
||||
copyImage.src = '/static/copy-button.svg';
|
||||
copyImage.alt = 'Copy';
|
||||
const copyButton = document.createElement("button");
|
||||
const copyImage = document.createElement("img");
|
||||
copyImage.src = "/static/copy-button.svg";
|
||||
copyImage.alt = "Copy";
|
||||
copyImage.width = 24;
|
||||
copyImage.height = 24;
|
||||
copyButton.appendChild(copyImage);
|
||||
copyButton.className = `hljs ${styles.codeCopyButton}`
|
||||
copyButton.addEventListener('click', () => {
|
||||
let textContent = preElement.textContent || '';
|
||||
copyButton.className = `hljs ${styles.codeCopyButton}`;
|
||||
copyButton.addEventListener("click", () => {
|
||||
let textContent = preElement.textContent || "";
|
||||
// Strip any leading $ characters
|
||||
textContent = textContent.replace(/^\$+/, '');
|
||||
textContent = textContent.replace(/^\$+/, "");
|
||||
// Remove 'Copy' if it's at the start of the string
|
||||
textContent = textContent.replace(/^Copy/, '');
|
||||
textContent = textContent.replace(/^Copy/, "");
|
||||
textContent = textContent.trim();
|
||||
navigator.clipboard.writeText(textContent);
|
||||
copyImage.src = '/static/copy-button-success.svg';
|
||||
copyImage.src = "/static/copy-button-success.svg";
|
||||
});
|
||||
preElement.prepend(copyButton);
|
||||
});
|
||||
|
@ -320,10 +360,10 @@ export default function ChatMessage(props: ChatMessageProps) {
|
|||
|
||||
renderMathInElement(messageRef.current, {
|
||||
delimiters: [
|
||||
{ left: '$$', right: '$$', display: true },
|
||||
{ left: '\\[', right: '\\]', display: true },
|
||||
{ left: '$', right: '$', display: false },
|
||||
{ left: '\\(', right: '\\)', display: false },
|
||||
{ left: "$$", right: "$$", display: true },
|
||||
{ left: "\\[", right: "\\]", display: true },
|
||||
{ left: "$", right: "$", display: false },
|
||||
{ left: "\\(", right: "\\)", display: false },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
@ -336,14 +376,18 @@ export default function ChatMessage(props: ChatMessageProps) {
|
|||
function formatDate(timestamp: string) {
|
||||
// Format date in HH:MM, DD MMM YYYY format
|
||||
let date = new Date(timestamp + "Z");
|
||||
let time_string = date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true }).toUpperCase();
|
||||
let date_string = date.toLocaleString('en-US', { year: 'numeric', month: 'short', day: '2-digit' }).replaceAll('-', ' ');
|
||||
let time_string = date
|
||||
.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: true })
|
||||
.toUpperCase();
|
||||
let date_string = date
|
||||
.toLocaleString("en-US", { year: "numeric", month: "short", day: "2-digit" })
|
||||
.replaceAll("-", " ");
|
||||
return `${time_string} on ${date_string}`;
|
||||
}
|
||||
|
||||
function renderTimeStamp(timestamp: string) {
|
||||
if (!timestamp.endsWith('Z')) {
|
||||
timestamp = timestamp + 'Z';
|
||||
if (!timestamp.endsWith("Z")) {
|
||||
timestamp = timestamp + "Z";
|
||||
}
|
||||
const messageDateTime = new Date(timestamp);
|
||||
const currentDateTime = new Date();
|
||||
|
@ -370,19 +414,21 @@ export default function ChatMessage(props: ChatMessageProps) {
|
|||
classes.push(styles[chatMessage.by]);
|
||||
|
||||
if (props.customClassName) {
|
||||
classes.push(styles[`${chatMessage.by}${props.customClassName}`])
|
||||
classes.push(styles[`${chatMessage.by}${props.customClassName}`]);
|
||||
}
|
||||
|
||||
return classes.join(' ');
|
||||
return classes.join(" ");
|
||||
}
|
||||
|
||||
function chatMessageWrapperClasses(chatMessage: SingleChatMessage) {
|
||||
let classes = [styles.chatMessageWrapper];
|
||||
classes.push(styles[chatMessage.by]);
|
||||
if (chatMessage.by === "khoj") {
|
||||
classes.push(`border-l-4 border-opacity-50 ${"border-l-" + props.borderLeftColor || "border-l-orange-400"}`);
|
||||
classes.push(
|
||||
`border-l-4 border-opacity-50 ${"border-l-" + props.borderLeftColor || "border-l-orange-400"}`,
|
||||
);
|
||||
}
|
||||
return classes.join(' ');
|
||||
return classes.join(" ");
|
||||
}
|
||||
|
||||
async function playTextToSpeech() {
|
||||
|
@ -425,7 +471,7 @@ export default function ChatMessage(props: ChatMessageProps) {
|
|||
const url = URL.createObjectURL(blob);
|
||||
await playAudio(url);
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
console.error("Error:", error);
|
||||
break; // Exit the loop on error
|
||||
}
|
||||
}
|
||||
|
@ -436,14 +482,14 @@ export default function ChatMessage(props: ChatMessageProps) {
|
|||
|
||||
async function fetchBlob(text: string) {
|
||||
const response = await fetch(`/api/chat/speech?text=${encodeURIComponent(text)}`, {
|
||||
method: 'POST',
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
|
||||
return await response.blob();
|
||||
|
@ -458,75 +504,97 @@ export default function ChatMessage(props: ChatMessageProps) {
|
|||
});
|
||||
}
|
||||
|
||||
const allReferences = constructAllReferences(props.chatMessage.context, props.chatMessage.onlineContext);
|
||||
const allReferences = constructAllReferences(
|
||||
props.chatMessage.context,
|
||||
props.chatMessage.onlineContext,
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={constructClasses(props.chatMessage)}
|
||||
onMouseLeave={(event) => setIsHovering(false)}
|
||||
onMouseEnter={(event) => setIsHovering(true)}
|
||||
onClick={props.chatMessage.by === "khoj" ? (event) => undefined : undefined}>
|
||||
onClick={props.chatMessage.by === "khoj" ? (event) => undefined : undefined}
|
||||
>
|
||||
<div className={chatMessageWrapperClasses(props.chatMessage)}>
|
||||
<div ref={messageRef} className={styles.chatMessage} dangerouslySetInnerHTML={{ __html: markdownRendered }} />
|
||||
<div
|
||||
ref={messageRef}
|
||||
className={styles.chatMessage}
|
||||
dangerouslySetInnerHTML={{ __html: markdownRendered }}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.teaserReferencesContainer}>
|
||||
<TeaserReferencesSection
|
||||
isMobileWidth={props.isMobileWidth}
|
||||
notesReferenceCardData={allReferences.notesReferenceCardData}
|
||||
onlineReferenceCardData={allReferences.onlineReferenceCardData} />
|
||||
onlineReferenceCardData={allReferences.onlineReferenceCardData}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.chatFooter}>
|
||||
{
|
||||
(isHovering || props.isMobileWidth || props.isLastMessage || isPlaying) &&
|
||||
(
|
||||
<>
|
||||
<div title={formatDate(props.chatMessage.created)} className={`text-gray-400 relative top-0 left-4`}>
|
||||
{renderTimeStamp(props.chatMessage.created)}
|
||||
</div>
|
||||
<div className={`${styles.chatButtons} shadow-sm`}>
|
||||
{
|
||||
(props.chatMessage.by === "khoj") &&
|
||||
(
|
||||
isPlaying ?
|
||||
(
|
||||
interrupted ?
|
||||
<InlineLoading iconClassName='p-0' className='m-0' />
|
||||
: <button title="Pause Speech" onClick={(event) => setInterrupted(true)}>
|
||||
<Pause alt="Pause Message" color='hsl(var(--muted-foreground))' />
|
||||
</button>
|
||||
)
|
||||
: <button title="Speak" onClick={(event) => playTextToSpeech()}>
|
||||
<SpeakerHigh alt="Speak Message" color='hsl(var(--muted-foreground))' />
|
||||
</button>
|
||||
{(isHovering || props.isMobileWidth || props.isLastMessage || isPlaying) && (
|
||||
<>
|
||||
<div
|
||||
title={formatDate(props.chatMessage.created)}
|
||||
className={`text-gray-400 relative top-0 left-4`}
|
||||
>
|
||||
{renderTimeStamp(props.chatMessage.created)}
|
||||
</div>
|
||||
<div className={`${styles.chatButtons} shadow-sm`}>
|
||||
{props.chatMessage.by === "khoj" &&
|
||||
(isPlaying ? (
|
||||
interrupted ? (
|
||||
<InlineLoading iconClassName="p-0" className="m-0" />
|
||||
) : (
|
||||
<button
|
||||
title="Pause Speech"
|
||||
onClick={(event) => setInterrupted(true)}
|
||||
>
|
||||
<Pause
|
||||
alt="Pause Message"
|
||||
color="hsl(var(--muted-foreground))"
|
||||
/>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
<button title="Copy" className={`${styles.copyButton}`} onClick={() => {
|
||||
) : (
|
||||
<button title="Speak" onClick={(event) => playTextToSpeech()}>
|
||||
<SpeakerHigh
|
||||
alt="Speak Message"
|
||||
color="hsl(var(--muted-foreground))"
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
title="Copy"
|
||||
className={`${styles.copyButton}`}
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(props.chatMessage.message);
|
||||
setCopySuccess(true);
|
||||
}}>
|
||||
{
|
||||
copySuccess ?
|
||||
<Copy alt="Copied Message" weight="fill" color='green' />
|
||||
: <Copy alt="Copy Message" color='hsl(var(--muted-foreground))' />
|
||||
}
|
||||
</button>
|
||||
{
|
||||
(props.chatMessage.by === "khoj") &&
|
||||
(
|
||||
props.chatMessage.intent ?
|
||||
<FeedbackButtons
|
||||
uquery={props.chatMessage.intent.query}
|
||||
kquery={props.chatMessage.message} />
|
||||
: <FeedbackButtons
|
||||
uquery={props.chatMessage.rawQuery || props.chatMessage.message}
|
||||
kquery={props.chatMessage.message} />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{copySuccess ? (
|
||||
<Copy alt="Copied Message" weight="fill" color="green" />
|
||||
) : (
|
||||
<Copy alt="Copy Message" color="hsl(var(--muted-foreground))" />
|
||||
)}
|
||||
</button>
|
||||
{props.chatMessage.by === "khoj" &&
|
||||
(props.chatMessage.intent ? (
|
||||
<FeedbackButtons
|
||||
uquery={props.chatMessage.intent.query}
|
||||
kquery={props.chatMessage.message}
|
||||
/>
|
||||
) : (
|
||||
<FeedbackButtons
|
||||
uquery={
|
||||
props.chatMessage.rawQuery || props.chatMessage.message
|
||||
}
|
||||
kquery={props.chatMessage.message}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { CircleNotch } from '@phosphor-icons/react';
|
||||
import { CircleNotch } from "@phosphor-icons/react";
|
||||
|
||||
interface LoadingProps {
|
||||
className?: string;
|
||||
|
@ -9,8 +9,18 @@ interface LoadingProps {
|
|||
export default function Loading(props: LoadingProps) {
|
||||
return (
|
||||
// NOTE: We can display usage tips here for casual learning moments.
|
||||
<div className={props.className || "bg-background opacity-50 flex items-center justify-center h-screen"}>
|
||||
<div>{props.message || "Loading" } <span><CircleNotch className="inline animate-spin h-5 w-5" /></span></div>
|
||||
<div
|
||||
className={
|
||||
props.className ||
|
||||
"bg-background opacity-50 flex items-center justify-center h-screen"
|
||||
}
|
||||
>
|
||||
<div>
|
||||
{props.message || "Loading"}{" "}
|
||||
<span>
|
||||
<CircleNotch className="inline animate-spin h-5 w-5" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -18,7 +28,12 @@ export default function Loading(props: LoadingProps) {
|
|||
export function InlineLoading(props: LoadingProps) {
|
||||
return (
|
||||
<button className={`${props.className}`}>
|
||||
<span>{props.message} <CircleNotch className={`inline animate-spin ${props.iconClassName ? props.iconClassName : 'h-5 w-5 mx-3'}`}/></span>
|
||||
<span>
|
||||
{props.message}{" "}
|
||||
<CircleNotch
|
||||
className={`inline animate-spin ${props.iconClassName ? props.iconClassName : "h-5 w-5 mx-3"}`}
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -6,9 +6,9 @@ import {
|
|||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import Link from 'next/link';
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import Link from "next/link";
|
||||
|
||||
export interface LoginPromptProps {
|
||||
loginRedirectMessage: string;
|
||||
|
@ -17,26 +17,31 @@ export interface LoginPromptProps {
|
|||
|
||||
export default function LoginPrompt(props: LoginPromptProps) {
|
||||
return (
|
||||
<AlertDialog
|
||||
open={true}
|
||||
onOpenChange={props.onOpenChange}>
|
||||
<AlertDialog open={true} onOpenChange={props.onOpenChange}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Sign in to Khoj to continue</AlertDialogTitle>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogDescription>{props.loginRedirectMessage}. By logging in, you agree to our <Link href="https://khoj.dev/terms-of-service">Terms of Service.</Link></AlertDialogDescription>
|
||||
<AlertDialogDescription>
|
||||
{props.loginRedirectMessage}. By logging in, you agree to our{" "}
|
||||
<Link href="https://khoj.dev/terms-of-service">Terms of Service.</Link>
|
||||
</AlertDialogDescription>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Dismiss</AlertDialogCancel>
|
||||
<AlertDialogAction className='bg-slate-400 hover:bg-slate-500'
|
||||
<AlertDialogAction
|
||||
className="bg-slate-400 hover:bg-slate-500"
|
||||
onClick={() => {
|
||||
window.location.href = `/login?next=${encodeURIComponent(window.location.pathname)}`;
|
||||
}}>
|
||||
<Link href={`/login?next=${encodeURIComponent(window.location.pathname)}`}> {/* Redirect to login page */}
|
||||
}}
|
||||
>
|
||||
<Link href={`/login?next=${encodeURIComponent(window.location.pathname)}`}>
|
||||
{" "}
|
||||
{/* Redirect to login page */}
|
||||
Login
|
||||
</Link>
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,10 +1,25 @@
|
|||
export function KhojLogoType() {
|
||||
return (
|
||||
<svg width="70" height="auto" viewBox="0 0 442 200" className="fill-zinc-950 dark:fill-zinc-300" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg
|
||||
width="70"
|
||||
height="auto"
|
||||
viewBox="0 0 442 200"
|
||||
className="fill-zinc-950 dark:fill-zinc-300"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0_45_75)">
|
||||
<path d="M57.9394 93.0404L67.5396 49.1063C68.7987 49.5268 71.7365 51.9442 74.7267 51.9442C78.5039 51.9442 79.2383 47.4246 86.1631 45.7955C91.6715 44.4817 96.3404 45.4276 99.0684 47.6349C99.0684 47.6349 99.6979 53.6259 101.167 55.4127C102.531 57.0418 104.629 56.4637 104.629 56.4637C107.672 70.8106 114.072 100.661 115.279 105.233C116.8 110.961 110.924 114.114 108.669 119.369C106.413 124.625 94.242 126.884 87.0549 127.62C79.8679 128.356 59.723 119.632 57.9394 117.215C56.1557 114.797 55.3688 109.857 55.1065 105.18C54.8442 101.449 56.8902 95.5104 57.9394 93.0404Z" fill="#FAE80B" />
|
||||
<path d="M57.9394 92.9879L62.2936 74.8046C63.5526 75.2251 66.9626 73.4383 71.4742 69.0764C74.1497 66.4488 79.8678 58.8812 86.0582 55.3601C90.5698 52.785 94.924 54.204 97.5995 56.4112C97.5995 56.4112 100.013 57.6199 102.846 57.6199C104.944 57.6199 104.629 56.4112 104.629 56.4112C107.672 70.7581 114.072 100.608 115.279 105.18C116.8 110.908 110.924 114.062 108.669 119.317C106.413 124.572 94.242 126.832 87.0549 127.568C79.8679 128.303 59.723 119.58 57.9394 117.162C56.1557 114.745 55.3688 109.805 55.1065 105.128C54.8442 101.449 56.8902 95.5104 57.9394 92.9879Z" fill="#FFCC09" />
|
||||
<path d="M69.3233 123.731C58.9886 121.104 55.8934 109.069 55.6311 103.393C58.7787 106.599 67.12 112.8 75.1464 111.959C85.2188 110.908 92.7207 106.862 100.852 100.503C108.983 94.1966 114.544 97.1921 114.544 106.809C114.544 116.426 107.619 118.108 105.521 122.47C103.423 126.884 82.281 127.042 69.3233 123.731Z" fill="#FBA719" />
|
||||
<path
|
||||
d="M57.9394 93.0404L67.5396 49.1063C68.7987 49.5268 71.7365 51.9442 74.7267 51.9442C78.5039 51.9442 79.2383 47.4246 86.1631 45.7955C91.6715 44.4817 96.3404 45.4276 99.0684 47.6349C99.0684 47.6349 99.6979 53.6259 101.167 55.4127C102.531 57.0418 104.629 56.4637 104.629 56.4637C107.672 70.8106 114.072 100.661 115.279 105.233C116.8 110.961 110.924 114.114 108.669 119.369C106.413 124.625 94.242 126.884 87.0549 127.62C79.8679 128.356 59.723 119.632 57.9394 117.215C56.1557 114.797 55.3688 109.857 55.1065 105.18C54.8442 101.449 56.8902 95.5104 57.9394 93.0404Z"
|
||||
fill="#FAE80B"
|
||||
/>
|
||||
<path
|
||||
d="M57.9394 92.9879L62.2936 74.8046C63.5526 75.2251 66.9626 73.4383 71.4742 69.0764C74.1497 66.4488 79.8678 58.8812 86.0582 55.3601C90.5698 52.785 94.924 54.204 97.5995 56.4112C97.5995 56.4112 100.013 57.6199 102.846 57.6199C104.944 57.6199 104.629 56.4112 104.629 56.4112C107.672 70.7581 114.072 100.608 115.279 105.18C116.8 110.908 110.924 114.062 108.669 119.317C106.413 124.572 94.242 126.832 87.0549 127.568C79.8679 128.303 59.723 119.58 57.9394 117.162C56.1557 114.745 55.3688 109.805 55.1065 105.128C54.8442 101.449 56.8902 95.5104 57.9394 92.9879Z"
|
||||
fill="#FFCC09"
|
||||
/>
|
||||
<path
|
||||
d="M69.3233 123.731C58.9886 121.104 55.8934 109.069 55.6311 103.393C58.7787 106.599 67.12 112.8 75.1464 111.959C85.2188 110.908 92.7207 106.862 100.852 100.503C108.983 94.1966 114.544 97.1921 114.544 106.809C114.544 116.426 107.619 118.108 105.521 122.47C103.423 126.884 82.281 127.042 69.3233 123.731Z"
|
||||
fill="#FBA719"
|
||||
/>
|
||||
<path d="M217.018 30.3459C218.946 32.2774 217.886 40.2931 217.886 45.315C217.886 48.4054 218.368 51.0129 218.368 53.4273C218.368 56.0348 217.886 58.5458 217.886 60.9602C218.368 62.9882 217.886 70.6177 218.946 71.1006C220.489 72.1629 222.031 66.9478 222.513 66.465C224.056 62.9883 222.995 65.1129 224.923 60.9602C226.948 57.0006 229.069 52.8479 230.611 49.9506C231.961 46.8602 234.082 42.4177 235.624 38.7479C237.167 36.3335 238.227 31.6979 240.637 29.7664C242.18 28.2212 247.193 28.2212 251.821 28.2212C255.291 28.7041 258.858 28.7041 259.919 30.6356C261.268 33.726 255.773 41.8383 254.231 45.2184C252.303 49.3712 250.76 51.8821 248.736 55.3588C248.254 56.4211 248.254 57.3869 247.675 58.9321C245.747 63.954 243.24 67.3341 240.637 72.5492C239.577 74.4807 238.709 76.9917 237.167 79.1163C236.106 82.0136 234.564 84.6211 234.564 86.6492C234.564 89.5464 236.588 93.6992 237.649 95.6307C241.12 102.294 244.687 108.861 248.157 115.911C248.639 117.264 248.639 118.809 249.7 120.354C251.242 124.313 253.267 127.404 255.677 132.426C257.219 135.999 261.172 142.566 260.304 145.463C258.762 149.037 254.616 148.071 250.182 148.071C244.687 148.071 240.541 148.071 238.709 147.009C236.685 145.463 235.142 139.959 233.6 137.061C230.129 128.949 228.587 124.313 224.634 115.911C223.574 112.821 222.031 109.731 220.489 107.316C218.464 104.709 218.464 112.821 217.886 116.394C217.886 120.837 217.886 124.313 217.886 126.342C217.886 132.909 219.428 144.015 216.536 147.009C213.451 149.616 198.894 149.037 196.773 145.463C196.291 143.532 196.773 137.544 196.773 132.426C197.255 127.404 196.773 124.313 196.773 119.871C196.291 108.861 196.291 98.7211 196.291 87.7115C196.291 83.5588 197.351 79.5992 196.773 76.5088C196.773 74.4807 196.291 73.0321 196.291 71.004C196.291 69.4588 196.773 68.3965 196.773 66.8513C196.773 63.3746 196.291 59.8013 196.291 56.3246C196.291 52.7513 196.773 49.2746 196.773 46.1842C196.773 41.7417 195.423 36.237 196.291 32.5671C196.773 31.0219 198.315 29.6698 198.894 29.0904C199.376 29.0904 199.376 29.0904 199.954 28.6075C203.811 27.2554 214.994 27.7384 217.018 30.3459Z" />
|
||||
<path d="M327.788 29.863C328.849 30.3459 328.849 32.2774 329.331 34.3054C329.813 38.265 329.331 46.3773 329.331 51.0129C328.849 58.5458 329.331 66.465 329.331 72.1629C329.331 82.1102 329.331 97.2724 329.331 105.868C329.331 114.946 329.331 127.5 328.849 138.22C328.849 142.663 329.331 146.14 327.306 147.685C324.896 149.23 310.628 149.23 308.796 147.685C306.772 145.657 307.736 139.09 307.736 134.647C307.736 132.619 307.736 130.687 307.736 128.659C307.736 119.002 307.254 109.924 307.736 98.9143C307.736 96.3067 308.218 93.4094 307.736 92.3471C306.193 90.8019 302.241 91.8642 299.156 91.8642C297.131 91.8642 292.6 90.319 290.576 92.3471C288.648 94.9546 289.515 102.005 289.515 106.447C289.515 117.457 289.997 123.155 289.997 134.647C289.997 140.635 290.479 145.657 287.587 147.685C284.502 149.713 272.451 149.23 270.427 147.202C267.342 144.305 267.824 132.619 267.824 125.569C267.824 103.936 268.884 87.9047 268.306 68.5896C268.306 64.63 268.788 60.4773 268.788 57.58C268.788 55.552 268.306 53.4273 267.728 50.5301C267.728 48.9849 268.21 46.9568 268.21 45.4116C268.692 39.9068 267.728 32.8568 268.692 30.8288C270.716 27.9315 286.334 27.352 288.744 30.4424C290.768 33.3397 289.226 39.4239 289.226 42.9972C288.744 46.9568 289.226 52.0752 289.226 57.58C289.226 59.1252 289.226 62.0225 289.226 64.6301C289.226 67.0444 289.226 70.6177 289.708 71.68C290.768 73.2252 295.878 73.2252 298.288 73.2252C300.313 73.2252 305.808 73.7081 306.868 72.1629C307.929 70.6177 307.929 62.0225 307.929 59.1253C307.929 51.013 307.929 46.3773 307.929 38.265C307.929 35.3677 307.447 32.2774 308.411 30.3459C309.471 28.3178 311.881 28.8006 313.906 28.3178C315.737 28.3178 325.764 27.2555 327.788 29.863Z" />
|
||||
<path d="M371.652 26.1931C379.75 25.7102 387.752 28.2212 392.765 30.6356C394.308 31.6979 397.778 35.2712 398.742 36.8164C401.345 40.2931 402.888 42.8041 403.755 47.3431C404.237 48.4054 404.816 48.8883 404.816 49.3711C404.816 49.854 404.816 50.4335 404.816 51.3993C405.298 53.8136 406.358 56.4211 406.84 57.9664C406.84 59.8979 406.84 61.4431 406.84 63.4711C406.84 65.0163 407.322 66.3684 407.322 67.9136C408.383 76.9916 407.804 87.7115 407.322 97.6587C407.322 104.226 407.322 110.696 406.262 116.781C405.201 122.961 403.659 129.529 402.309 132.909C401.827 133.971 400.285 136.482 399.224 137.544L398.742 138.027C397.2 140.442 394.308 142.47 391.223 144.594C389.68 145.463 389.198 145.946 387.752 146.526C386.21 147.009 384.667 147.009 382.064 147.588C380.714 147.588 379.172 148.65 377.629 148.65C374.159 149.133 369.531 149.133 367.121 149.133C364.036 148.65 360.469 147.588 357.481 146.043C356.999 146.043 356.42 145.174 356.42 145.174C354.878 144.691 352.853 143.629 351.986 143.146C349.961 141.6 347.358 138.51 345.816 136.579C342.345 130.591 339.838 121.416 339.26 112.821C338.778 106.254 338.778 97.6588 338.778 92.154C338.296 88.0012 338.778 83.5588 339.26 79.5992C339.26 73.9013 339.26 67.9136 339.742 62.8917C340.224 60.2841 340.803 57.8698 341.285 55.8417C341.285 52.7513 341.285 50.3369 341.767 47.7294C343.309 43.287 346.78 35.6575 350.347 32.5671C351.407 32.0842 352.757 31.0219 354.299 30.1527C358.445 28.1246 364.422 26.5795 370.978 26H371.652V26.1931ZM361.53 59.8979C359.987 72.6458 360.469 86.1663 361.048 99.3005C361.53 109.441 362.108 124.893 366.736 128.563C366.736 129.046 368.085 129.625 369.146 130.108C373.291 131.46 376.665 130.591 379.75 128.563C380.811 127.5 382.161 125.955 382.643 124.99C384.185 122.575 385.245 115.525 385.728 110.89C385.728 106.93 385.728 101.908 386.21 96.3067C386.21 85.2971 385.728 74.0944 384.667 64.6301C384.185 58.0629 383.607 52.0753 381.582 48.9849C380.232 46.9568 378.111 45.4116 376.087 44.8322C373.677 43.963 372.616 44.8322 371.652 44.8322H371.17C364.615 45.7979 362.59 52.365 361.53 59.8979Z" />
|
||||
|
@ -32,11 +47,26 @@ export function KhojLogoType() {
|
|||
|
||||
export function KhojLogo() {
|
||||
return (
|
||||
<svg width="200" height="200" viewBox="0 0 200 200" className="fill-zinc-950 dark:fill-zinc-300" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg
|
||||
width="200"
|
||||
height="200"
|
||||
viewBox="0 0 200 200"
|
||||
className="fill-zinc-950 dark:fill-zinc-300"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0_45_75)">
|
||||
<path d="M57.9394 93.0404L67.5396 49.1063C68.7987 49.5268 71.7365 51.9442 74.7267 51.9442C78.5039 51.9442 79.2383 47.4246 86.1631 45.7955C91.6715 44.4817 96.3404 45.4276 99.0684 47.6349C99.0684 47.6349 99.6979 53.6259 101.167 55.4127C102.531 57.0418 104.629 56.4637 104.629 56.4637C107.672 70.8106 114.072 100.661 115.279 105.233C116.8 110.961 110.924 114.114 108.669 119.369C106.413 124.625 94.242 126.884 87.0549 127.62C79.8679 128.356 59.723 119.632 57.9394 117.215C56.1557 114.797 55.3688 109.857 55.1065 105.18C54.8442 101.449 56.8902 95.5104 57.9394 93.0404Z" fill="#FAE80B" />
|
||||
<path d="M57.9394 92.9879L62.2936 74.8046C63.5526 75.2251 66.9626 73.4383 71.4742 69.0764C74.1497 66.4488 79.8678 58.8812 86.0582 55.3601C90.5698 52.785 94.924 54.204 97.5995 56.4112C97.5995 56.4112 100.013 57.6199 102.846 57.6199C104.944 57.6199 104.629 56.4112 104.629 56.4112C107.672 70.7581 114.072 100.608 115.279 105.18C116.8 110.908 110.924 114.062 108.669 119.317C106.413 124.572 94.242 126.832 87.0549 127.568C79.8679 128.303 59.723 119.58 57.9394 117.162C56.1557 114.745 55.3688 109.805 55.1065 105.128C54.8442 101.449 56.8902 95.5104 57.9394 92.9879Z" fill="#FFCC09" />
|
||||
<path d="M69.3233 123.731C58.9886 121.104 55.8934 109.069 55.6311 103.393C58.7787 106.599 67.12 112.8 75.1464 111.959C85.2188 110.908 92.7207 106.862 100.852 100.503C108.983 94.1966 114.544 97.1921 114.544 106.809C114.544 116.426 107.619 118.108 105.521 122.47C103.423 126.884 82.281 127.042 69.3233 123.731Z" fill="#FBA719" />
|
||||
<path
|
||||
d="M57.9394 93.0404L67.5396 49.1063C68.7987 49.5268 71.7365 51.9442 74.7267 51.9442C78.5039 51.9442 79.2383 47.4246 86.1631 45.7955C91.6715 44.4817 96.3404 45.4276 99.0684 47.6349C99.0684 47.6349 99.6979 53.6259 101.167 55.4127C102.531 57.0418 104.629 56.4637 104.629 56.4637C107.672 70.8106 114.072 100.661 115.279 105.233C116.8 110.961 110.924 114.114 108.669 119.369C106.413 124.625 94.242 126.884 87.0549 127.62C79.8679 128.356 59.723 119.632 57.9394 117.215C56.1557 114.797 55.3688 109.857 55.1065 105.18C54.8442 101.449 56.8902 95.5104 57.9394 93.0404Z"
|
||||
fill="#FAE80B"
|
||||
/>
|
||||
<path
|
||||
d="M57.9394 92.9879L62.2936 74.8046C63.5526 75.2251 66.9626 73.4383 71.4742 69.0764C74.1497 66.4488 79.8678 58.8812 86.0582 55.3601C90.5698 52.785 94.924 54.204 97.5995 56.4112C97.5995 56.4112 100.013 57.6199 102.846 57.6199C104.944 57.6199 104.629 56.4112 104.629 56.4112C107.672 70.7581 114.072 100.608 115.279 105.18C116.8 110.908 110.924 114.062 108.669 119.317C106.413 124.572 94.242 126.832 87.0549 127.568C79.8679 128.303 59.723 119.58 57.9394 117.162C56.1557 114.745 55.3688 109.805 55.1065 105.128C54.8442 101.449 56.8902 95.5104 57.9394 92.9879Z"
|
||||
fill="#FFCC09"
|
||||
/>
|
||||
<path
|
||||
d="M69.3233 123.731C58.9886 121.104 55.8934 109.069 55.6311 103.393C58.7787 106.599 67.12 112.8 75.1464 111.959C85.2188 110.908 92.7207 106.862 100.852 100.503C108.983 94.1966 114.544 97.1921 114.544 106.809C114.544 116.426 107.619 118.108 105.521 122.47C103.423 126.884 82.281 127.042 69.3233 123.731Z"
|
||||
fill="#FBA719"
|
||||
/>
|
||||
<path d="M46.6374 143.679C43.5946 143.679 41.2864 142.838 39.6601 141.104C36.7223 138.004 37.5092 133.379 37.5617 133.169C37.719 132.275 52.3031 43.8816 53.667 35.2104C54.9261 27.1698 64.0542 25.383 68.7232 25.5932L68.6183 28.7464C68.5658 28.7464 65.733 28.6413 62.8476 29.5347C59.2803 30.6383 57.2344 32.6878 56.7622 35.6834C55.4507 44.3546 40.8142 132.801 40.6568 133.694C40.6568 133.747 40.1322 137.005 41.9684 138.95C43.2799 140.316 45.4832 140.789 48.6308 140.368L48.9981 143.469C48.2112 143.627 47.4243 143.679 46.6374 143.679Z" />
|
||||
<path d="M106.023 33.371H102.928C102.98 33.2133 102.98 33.0031 102.98 32.8455V15.2403C102.98 13.506 101.564 12.0871 99.8324 12.0871H89.4977C90.8616 10.8784 91.7535 9.09161 91.7535 7.09461C91.7535 3.41592 88.7632 0.42041 85.091 0.42041C81.4188 0.42041 78.4285 3.41592 78.4285 7.09461C78.4285 9.09161 79.2679 10.8784 80.6843 12.0871H70.8217C69.0905 12.0871 67.6741 13.506 67.6741 15.2403V32.8455C67.6741 33.0557 67.6741 33.2133 67.7265 33.371H64.0543C61.5887 33.371 59.5427 35.4205 59.5427 37.8905C59.5427 40.4131 61.5887 42.4101 64.0543 42.4101H106.075C108.541 42.4101 110.587 40.3605 110.587 37.8905C110.534 35.368 108.541 33.371 106.023 33.371ZM85.0385 3.52103C86.9795 3.52103 88.5534 5.0976 88.5534 7.04205C88.5534 8.98651 86.9795 10.5631 85.0385 10.5631C83.0975 10.5631 81.5237 8.98651 81.5237 7.04205C81.5237 5.0976 83.0975 3.52103 85.0385 3.52103Z" />
|
||||
<path d="M123.177 143.679C122.443 143.679 121.656 143.627 120.816 143.522L121.184 140.421C124.331 140.841 126.535 140.316 127.846 139.002C129.682 137.058 129.158 133.799 129.158 133.799C129 132.906 114.364 44.4596 113.052 35.7884C112.58 32.7929 110.534 30.7433 106.967 29.6397C104.082 28.7463 101.249 28.8514 101.196 28.8514L101.091 25.6983C105.76 25.4881 114.941 27.2749 116.147 35.3154C117.459 43.9866 132.095 132.38 132.253 133.274C132.305 133.431 133.092 138.056 130.154 141.157C128.528 142.786 126.22 143.679 123.177 143.679Z" />
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useAuthenticatedData } from '@/app/common/auth';
|
||||
import React, { useEffect } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import { useAuthenticatedData } from "@/app/common/auth";
|
||||
import React, { useEffect } from "react";
|
||||
import useSWR from "swr";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
|
@ -12,8 +12,7 @@ import {
|
|||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
|
||||
|
||||
import styles from './modelPicker.module.css';
|
||||
import styles from "./modelPicker.module.css";
|
||||
|
||||
export interface Model {
|
||||
id: number;
|
||||
|
@ -23,10 +22,10 @@ export interface Model {
|
|||
// Custom fetcher function to fetch options
|
||||
const fetchOptionsRequest = async (url: string) => {
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
return response.json();
|
||||
};
|
||||
|
@ -35,21 +34,21 @@ export const useOptionsRequest = (url: string) => {
|
|||
const { data, error } = useSWR<Model[]>(url, fetchOptionsRequest);
|
||||
|
||||
return {
|
||||
data,
|
||||
isLoading: !error && !data,
|
||||
isError: error,
|
||||
data,
|
||||
isLoading: !error && !data,
|
||||
isError: error,
|
||||
};
|
||||
};
|
||||
|
||||
const fetchSelectedModel = async (url: string) => {
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
method: "GET",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
return response.json();
|
||||
}
|
||||
};
|
||||
|
||||
export const useSelectedModel = (url: string) => {
|
||||
const { data, error } = useSWR<Model>(url, fetchSelectedModel);
|
||||
|
@ -58,8 +57,8 @@ export const useSelectedModel = (url: string) => {
|
|||
data,
|
||||
isLoading: !error && !data,
|
||||
isError: error,
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
interface ModelPickerProps {
|
||||
disabled?: boolean;
|
||||
|
@ -68,8 +67,8 @@ interface ModelPickerProps {
|
|||
}
|
||||
|
||||
export const ModelPicker: React.FC<any> = (props: ModelPickerProps) => {
|
||||
const { data: models } = useOptionsRequest('/api/model/chat/options');
|
||||
const { data: selectedModel } = useSelectedModel('/api/model/chat');
|
||||
const { data: models } = useOptionsRequest("/api/model/chat/options");
|
||||
const { data: selectedModel } = useSelectedModel("/api/model/chat");
|
||||
const [openLoginDialog, setOpenLoginDialog] = React.useState(false);
|
||||
|
||||
let userData = useAuthenticatedData();
|
||||
|
@ -96,14 +95,17 @@ export const ModelPicker: React.FC<any> = (props: ModelPickerProps) => {
|
|||
props.setModelUsed(model);
|
||||
}
|
||||
|
||||
fetch('/api/model/chat' + '?id=' + String(model.id), { method: 'POST', body: JSON.stringify(model) })
|
||||
fetch("/api/model/chat" + "?id=" + String(model.id), {
|
||||
method: "POST",
|
||||
body: JSON.stringify(model),
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to select model');
|
||||
throw new Error("Failed to select model");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to select model', error);
|
||||
console.error("Failed to select model", error);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -116,15 +118,19 @@ export const ModelPicker: React.FC<any> = (props: ModelPickerProps) => {
|
|||
|
||||
return (
|
||||
<div className={styles.modelPicker}>
|
||||
<select className={styles.modelPicker} onChange={(e) => {
|
||||
const selectedModelId = Number(e.target.value);
|
||||
const selectedModel = models.find((model) => model.id === selectedModelId);
|
||||
if (selectedModel) {
|
||||
onSelect(selectedModel);
|
||||
} else {
|
||||
console.error('Selected model not found', e.target.value);
|
||||
}
|
||||
}} disabled={props.disabled}>
|
||||
<select
|
||||
className={styles.modelPicker}
|
||||
onChange={(e) => {
|
||||
const selectedModelId = Number(e.target.value);
|
||||
const selectedModel = models.find((model) => model.id === selectedModelId);
|
||||
if (selectedModel) {
|
||||
onSelect(selectedModel);
|
||||
} else {
|
||||
console.error("Selected model not found", e.target.value);
|
||||
}
|
||||
}}
|
||||
disabled={props.disabled}
|
||||
>
|
||||
{models?.map((model) => (
|
||||
<option key={model.id} value={model.id} selected={isSelected(model)}>
|
||||
{model.chat_model}
|
||||
|
@ -134,18 +140,24 @@ export const ModelPicker: React.FC<any> = (props: ModelPickerProps) => {
|
|||
<AlertDialog open={openLoginDialog} onOpenChange={setOpenLoginDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>You must be logged in to configure your model.</AlertDialogTitle>
|
||||
<AlertDialogDescription>Once you create an account with Khoj, you can configure your model and use a whole suite of other features. Check out our <a href="https://docs.khoj.dev/">documentation</a> to learn more.
|
||||
</AlertDialogDescription>
|
||||
<AlertDialogTitle>
|
||||
You must be logged in to configure your model.
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Once you create an account with Khoj, you can configure your model and
|
||||
use a whole suite of other features. Check out our{" "}
|
||||
<a href="https://docs.khoj.dev/">documentation</a> to learn more.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() => {
|
||||
window.location.href = window.location.origin + '/login';
|
||||
}}>
|
||||
Sign in
|
||||
</AlertDialogAction>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() => {
|
||||
window.location.href = window.location.origin + "/login";
|
||||
}}
|
||||
>
|
||||
Sign in
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
'use client'
|
||||
"use client";
|
||||
|
||||
import styles from './navMenu.module.css';
|
||||
import Link from 'next/link';
|
||||
import { useAuthenticatedData } from '@/app/common/auth';
|
||||
import { useState, useEffect } from 'react';
|
||||
import styles from "./navMenu.module.css";
|
||||
import Link from "next/link";
|
||||
import { useAuthenticatedData } from "@/app/common/auth";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
|
||||
|
||||
import {
|
||||
|
@ -22,10 +22,20 @@ import {
|
|||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Moon, Sun, UserCircle, User, Robot, MagnifyingGlass, Question, GearFine, ArrowRight, UsersFour } from '@phosphor-icons/react';
|
||||
import {
|
||||
Moon,
|
||||
Sun,
|
||||
UserCircle,
|
||||
User,
|
||||
Robot,
|
||||
MagnifyingGlass,
|
||||
Question,
|
||||
GearFine,
|
||||
ArrowRight,
|
||||
UsersFour,
|
||||
} from "@phosphor-icons/react";
|
||||
|
||||
export default function NavMenu() {
|
||||
|
||||
const userData = useAuthenticatedData();
|
||||
const [isMobileWidth, setIsMobileWidth] = useState(false);
|
||||
const [darkMode, setDarkMode] = useState(false);
|
||||
|
@ -34,23 +44,21 @@ export default function NavMenu() {
|
|||
useEffect(() => {
|
||||
setIsMobileWidth(window.innerWidth < 768);
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
window.addEventListener("resize", () => {
|
||||
setIsMobileWidth(window.innerWidth < 768);
|
||||
});
|
||||
|
||||
if (localStorage.getItem('theme') === 'dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
if (localStorage.getItem("theme") === "dark") {
|
||||
document.documentElement.classList.add("dark");
|
||||
setDarkMode(true);
|
||||
} else if (localStorage.getItem('theme') === 'light') {
|
||||
document.documentElement.classList.remove('dark');
|
||||
} else if (localStorage.getItem("theme") === "light") {
|
||||
document.documentElement.classList.remove("dark");
|
||||
setDarkMode(false);
|
||||
} else {
|
||||
const mq = window.matchMedia(
|
||||
"(prefers-color-scheme: dark)"
|
||||
);
|
||||
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
|
||||
if (mq.matches) {
|
||||
document.documentElement.classList.add('dark');
|
||||
document.documentElement.classList.add("dark");
|
||||
setDarkMode(true);
|
||||
}
|
||||
}
|
||||
|
@ -58,7 +66,6 @@ export default function NavMenu() {
|
|||
setInitialLoadDone(true);
|
||||
}, []);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (!initialLoadDone) return;
|
||||
toggleDarkMode(darkMode);
|
||||
|
@ -66,206 +73,221 @@ export default function NavMenu() {
|
|||
|
||||
function toggleDarkMode(darkMode: boolean) {
|
||||
if (darkMode) {
|
||||
document.documentElement.classList.add('dark');
|
||||
document.documentElement.classList.add("dark");
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
document.documentElement.classList.remove("dark");
|
||||
}
|
||||
localStorage.setItem('theme', darkMode ? 'dark' : 'light');
|
||||
localStorage.setItem("theme", darkMode ? "dark" : "light");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.titleBar}>
|
||||
{
|
||||
isMobileWidth ?
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
{
|
||||
userData ?
|
||||
<Avatar className={`h-10 w-10 border-2 ${userData.is_active ? "border-yellow-500" : "border-stone-700 dark:border-stone-300"}`}>
|
||||
<AvatarImage src={userData?.photo} alt="user profile" />
|
||||
<AvatarFallback className="bg-transparent hover:bg-muted">
|
||||
{userData?.username[0].toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
:
|
||||
<UserCircle className="h-10 w-10" />
|
||||
}
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className='gap-2'>
|
||||
<DropdownMenuItem onClick={() => setDarkMode(!darkMode)} className='w-full cursor-pointer'>
|
||||
<div
|
||||
className="flex flex-rows">
|
||||
{darkMode ? <Sun className="w-6 h-6" /> : <Moon className="w-6 h-6" />}
|
||||
<p className="ml-3 font-semibold">{darkMode ? 'Light Mode' : 'Dark Mode'}</p>
|
||||
{isMobileWidth ? (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
{userData ? (
|
||||
<Avatar
|
||||
className={`h-10 w-10 border-2 ${userData.is_active ? "border-yellow-500" : "border-stone-700 dark:border-stone-300"}`}
|
||||
>
|
||||
<AvatarImage src={userData?.photo} alt="user profile" />
|
||||
<AvatarFallback className="bg-transparent hover:bg-muted">
|
||||
{userData?.username[0].toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
) : (
|
||||
<UserCircle className="h-10 w-10" />
|
||||
)}
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="gap-2">
|
||||
<DropdownMenuItem
|
||||
onClick={() => setDarkMode(!darkMode)}
|
||||
className="w-full cursor-pointer"
|
||||
>
|
||||
<div className="flex flex-rows">
|
||||
{darkMode ? (
|
||||
<Sun className="w-6 h-6" />
|
||||
) : (
|
||||
<Moon className="w-6 h-6" />
|
||||
)}
|
||||
<p className="ml-3 font-semibold">
|
||||
{darkMode ? "Light Mode" : "Dark Mode"}
|
||||
</p>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Link href="/agents" className="no-underline w-full">
|
||||
<div className="flex flex-rows">
|
||||
<UsersFour className="w-6 h-6" />
|
||||
<p className="ml-3 font-semibold">Agents</p>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Link href="/automations" className="no-underline w-full">
|
||||
<div className="flex flex-rows">
|
||||
<Robot className="w-6 h-6" />
|
||||
<p className="ml-3 font-semibold">Automations</p>
|
||||
</div>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
{userData && (
|
||||
<DropdownMenuItem>
|
||||
<Link href="/search" className="no-underline w-full">
|
||||
<div className="flex flex-rows">
|
||||
<MagnifyingGlass className="w-6 h-6" />
|
||||
<p className="ml-3 font-semibold">Search</p>
|
||||
</div>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
{userData && (
|
||||
<DropdownMenuItem>
|
||||
<Link href="/settings" className="no-underline w-full">
|
||||
<div className="flex flex-rows">
|
||||
<GearFine className="w-6 h-6" />
|
||||
<p className="ml-3 font-semibold">Settings</p>
|
||||
</div>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem>
|
||||
<Link href="https://docs.khoj.dev" className="no-underline w-full">
|
||||
<div className="flex flex-rows">
|
||||
<Question className="w-6 h-6" />
|
||||
<p className="ml-3 font-semibold">Help</p>
|
||||
</div>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
{userData ? (
|
||||
<DropdownMenuItem>
|
||||
<Link href="/auth/logout" className="no-underline w-full">
|
||||
<div className="flex flex-rows">
|
||||
<ArrowRight className="w-6 h-6" />
|
||||
<p className="ml-3 font-semibold">Logout</p>
|
||||
</div>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
) : (
|
||||
<DropdownMenuItem>
|
||||
<Link href="/auth/login" className="no-underline w-full">
|
||||
<div className="flex flex-rows">
|
||||
<ArrowRight className="w-6 h-6" />
|
||||
<p className="ml-3 font-semibold">Login</p>
|
||||
</div>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : (
|
||||
<Menubar className="border-none">
|
||||
<MenubarMenu>
|
||||
<MenubarTrigger>
|
||||
{userData ? (
|
||||
<Avatar
|
||||
className={`h-10 w-10 border-2 ${userData.is_active ? "border-yellow-500" : "border-stone-700 dark:border-stone-300"}`}
|
||||
>
|
||||
<AvatarImage src={userData?.photo} alt="user profile" />
|
||||
<AvatarFallback className="bg-transparent hover:bg-muted">
|
||||
{userData?.username[0].toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
) : (
|
||||
<UserCircle className="w-10 h-10" />
|
||||
)}
|
||||
</MenubarTrigger>
|
||||
<MenubarContent align="end" className="rounded-xl gap-2">
|
||||
<MenubarItem
|
||||
onClick={() => setDarkMode(!darkMode)}
|
||||
className="w-full hover:cursor-pointer"
|
||||
>
|
||||
<div className="flex flex-rows">
|
||||
{darkMode ? (
|
||||
<Sun className="w-6 h-6" />
|
||||
) : (
|
||||
<Moon className="w-6 h-6" />
|
||||
)}
|
||||
<p className="ml-3 font-semibold">
|
||||
{darkMode ? "Light Mode" : "Dark Mode"}
|
||||
</p>
|
||||
</div>
|
||||
</MenubarItem>
|
||||
<MenubarItem>
|
||||
<Link href="/agents" className="no-underline w-full">
|
||||
<div className="flex flex-rows">
|
||||
<UsersFour className="w-6 h-6" />
|
||||
<p className="ml-3 font-semibold">Agents</p>
|
||||
</div>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
</MenubarItem>
|
||||
<MenubarItem>
|
||||
<Link href="/automations" className="no-underline w-full">
|
||||
<div className="flex flex-rows">
|
||||
<Robot className="w-6 h-6" />
|
||||
<p className="ml-3 font-semibold">Automations</p>
|
||||
</div>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
{
|
||||
userData &&
|
||||
<DropdownMenuItem>
|
||||
</MenubarItem>
|
||||
{userData && (
|
||||
<MenubarItem>
|
||||
<Link href="/search" className="no-underline w-full">
|
||||
<div className="flex flex-rows">
|
||||
<MagnifyingGlass className="w-6 h-6" />
|
||||
<p className="ml-3 font-semibold">Search</p>
|
||||
</div>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
}
|
||||
</MenubarItem>
|
||||
)}
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
{userData &&
|
||||
<DropdownMenuItem>
|
||||
<MenubarSeparator className="dark:bg-white height-[2px] bg-black" />
|
||||
<MenubarItem>
|
||||
<Link
|
||||
href="https://docs.khoj.dev"
|
||||
className="no-underline w-full"
|
||||
>
|
||||
<div className="flex flex-rows">
|
||||
<Question className="w-6 h-6" />
|
||||
<p className="ml-3 font-semibold">Help</p>
|
||||
</div>
|
||||
</Link>
|
||||
</MenubarItem>
|
||||
{userData && (
|
||||
<MenubarItem>
|
||||
<Link href="/settings" className="no-underline w-full">
|
||||
<div className="flex flex-rows">
|
||||
<GearFine className="w-6 h-6" />
|
||||
<p className="ml-3 font-semibold">Settings</p>
|
||||
</div>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
}
|
||||
<DropdownMenuItem>
|
||||
<Link href="https://docs.khoj.dev" className="no-underline w-full">
|
||||
<div className="flex flex-rows">
|
||||
<Question className="w-6 h-6" />
|
||||
<p className="ml-3 font-semibold">Help</p>
|
||||
</div>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
{
|
||||
userData ?
|
||||
<DropdownMenuItem>
|
||||
<Link href="/auth/logout" className="no-underline w-full">
|
||||
<div className="flex flex-rows">
|
||||
<ArrowRight className="w-6 h-6" />
|
||||
<p className="ml-3 font-semibold">Logout</p>
|
||||
</div>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
:
|
||||
<DropdownMenuItem>
|
||||
<Link href="/auth/login" className="no-underline w-full">
|
||||
<div className="flex flex-rows">
|
||||
<ArrowRight className="w-6 h-6" />
|
||||
<p className="ml-3 font-semibold">Login</p>
|
||||
</div>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
}
|
||||
</MenubarItem>
|
||||
)}
|
||||
{userData ? (
|
||||
<MenubarItem>
|
||||
<Link href="/auth/logout" className="no-underline w-full">
|
||||
<div className="flex flex-rows">
|
||||
<ArrowRight className="w-6 h-6" />
|
||||
<p className="ml-3 font-semibold">Logout</p>
|
||||
</div>
|
||||
</Link>
|
||||
</MenubarItem>
|
||||
) : (
|
||||
<MenubarItem>
|
||||
<Link href="/auth/login" className="no-underline w-full">
|
||||
<div className="flex flex-rows">
|
||||
<ArrowRight className="w-6 h-6" />
|
||||
<p className="ml-3 font-semibold">Login</p>
|
||||
</div>
|
||||
</Link>
|
||||
</MenubarItem>
|
||||
)}
|
||||
</>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
:
|
||||
<Menubar className='border-none'>
|
||||
<MenubarMenu>
|
||||
<MenubarTrigger>
|
||||
{
|
||||
userData ?
|
||||
<Avatar className={`h-10 w-10 border-2 ${userData.is_active ? "border-yellow-500" : "border-stone-700 dark:border-stone-300"}`}>
|
||||
<AvatarImage src={userData?.photo} alt="user profile" />
|
||||
<AvatarFallback className="bg-transparent hover:bg-muted">
|
||||
{userData?.username[0].toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
:
|
||||
<UserCircle className="w-10 h-10" />
|
||||
}
|
||||
</MenubarTrigger>
|
||||
<MenubarContent align="end" className="rounded-xl gap-2">
|
||||
<MenubarItem onClick={() => setDarkMode(!darkMode)} className="w-full hover:cursor-pointer">
|
||||
<div
|
||||
className="flex flex-rows">
|
||||
{darkMode ? <Sun className="w-6 h-6" /> : <Moon className="w-6 h-6" />}
|
||||
<p className="ml-3 font-semibold">{darkMode ? 'Light Mode' : 'Dark Mode'}</p>
|
||||
</div>
|
||||
</MenubarItem>
|
||||
<MenubarItem>
|
||||
<Link href="/agents" className="no-underline w-full">
|
||||
<div className="flex flex-rows">
|
||||
<UsersFour className="w-6 h-6" />
|
||||
<p className="ml-3 font-semibold">Agents</p>
|
||||
</div>
|
||||
</Link>
|
||||
</MenubarItem>
|
||||
<MenubarItem>
|
||||
<Link href="/automations" className="no-underline w-full">
|
||||
<div className="flex flex-rows">
|
||||
<Robot className="w-6 h-6" />
|
||||
<p className="ml-3 font-semibold">Automations</p>
|
||||
</div>
|
||||
</Link>
|
||||
</MenubarItem>
|
||||
{
|
||||
userData &&
|
||||
<MenubarItem>
|
||||
<Link href="/search" className="no-underline w-full">
|
||||
<div className="flex flex-rows">
|
||||
<MagnifyingGlass className="w-6 h-6" />
|
||||
<p className="ml-3 font-semibold">Search</p>
|
||||
</div>
|
||||
</Link>
|
||||
</MenubarItem>
|
||||
}
|
||||
<>
|
||||
<MenubarSeparator className="dark:bg-white height-[2px] bg-black" />
|
||||
<MenubarItem>
|
||||
<Link href="https://docs.khoj.dev" className="no-underline w-full">
|
||||
<div className="flex flex-rows">
|
||||
<Question className="w-6 h-6" />
|
||||
<p className="ml-3 font-semibold">Help</p>
|
||||
</div>
|
||||
</Link>
|
||||
</MenubarItem>
|
||||
{
|
||||
userData &&
|
||||
<MenubarItem>
|
||||
<Link href="/settings" className="no-underline w-full">
|
||||
<div className="flex flex-rows">
|
||||
<GearFine className="w-6 h-6" />
|
||||
<p className="ml-3 font-semibold">Settings</p>
|
||||
</div>
|
||||
</Link>
|
||||
</MenubarItem>
|
||||
}
|
||||
{
|
||||
userData ?
|
||||
<MenubarItem>
|
||||
<Link href="/auth/logout" className="no-underline w-full">
|
||||
<div className="flex flex-rows">
|
||||
<ArrowRight className="w-6 h-6" />
|
||||
<p className="ml-3 font-semibold">Logout</p>
|
||||
</div>
|
||||
</Link>
|
||||
</MenubarItem>
|
||||
:
|
||||
<MenubarItem>
|
||||
<Link href="/auth/login" className="no-underline w-full">
|
||||
<div className="flex flex-rows">
|
||||
<ArrowRight className="w-6 h-6" />
|
||||
<p className="ml-3 font-semibold">Login</p>
|
||||
</div>
|
||||
</Link>
|
||||
</MenubarItem>
|
||||
}
|
||||
</>
|
||||
</MenubarContent>
|
||||
</MenubarMenu>
|
||||
</Menubar>
|
||||
}
|
||||
</MenubarContent>
|
||||
</MenubarMenu>
|
||||
</Menubar>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,58 +1,55 @@
|
|||
import React from 'react';
|
||||
import { ArrowRight } from '@phosphor-icons/react';
|
||||
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { Button } from '@/components/ui/button';
|
||||
import React from "react";
|
||||
import { ArrowRight } from "@phosphor-icons/react";
|
||||
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
interface ProfileCardProps {
|
||||
name: string;
|
||||
avatar: JSX.Element;
|
||||
link: string;
|
||||
description?: string; // Optional description field
|
||||
name: string;
|
||||
avatar: JSX.Element;
|
||||
link: string;
|
||||
description?: string; // Optional description field
|
||||
}
|
||||
|
||||
const ProfileCard: React.FC<ProfileCardProps> = ({ name, avatar, link, description }) => {
|
||||
return (
|
||||
<div className="relative group flex">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" className="flex items-center justify-center">
|
||||
{avatar}
|
||||
<div>{name}</div>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div className='w-80 h-30'>
|
||||
{/* <div className="absolute left-0 bottom-full w-80 h-30 p-2 pb-4 bg-white border border-gray-300 rounded-lg shadow-lg opacity-0 group-hover:opacity-100 transition-opacity duration-300"> */}
|
||||
<a href={link} target="_blank" rel="noreferrer" className="mt-1 ml-2 block no-underline">
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
{avatar}
|
||||
<div className="mr-2 mt-1 flex justify-center items-center text-sm font-semibold text-gray-800">
|
||||
{name}
|
||||
<ArrowRight weight="bold" className='ml-1' />
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{description && (
|
||||
<p className="mt-2 ml-6 text-sm text-gray-600 line-clamp-2">
|
||||
{description || 'A Khoj agent'}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="relative group flex">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" className="flex items-center justify-center">
|
||||
{avatar}
|
||||
<div>{name}</div>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div className="w-80 h-30">
|
||||
{/* <div className="absolute left-0 bottom-full w-80 h-30 p-2 pb-4 bg-white border border-gray-300 rounded-lg shadow-lg opacity-0 group-hover:opacity-100 transition-opacity duration-300"> */}
|
||||
<a
|
||||
href={link}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="mt-1 ml-2 block no-underline"
|
||||
>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
{avatar}
|
||||
<div className="mr-2 mt-1 flex justify-center items-center text-sm font-semibold text-gray-800">
|
||||
{name}
|
||||
<ArrowRight weight="bold" className="ml-1" />
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{description && (
|
||||
<p className="mt-2 ml-6 text-sm text-gray-600 line-clamp-2">
|
||||
{description || "A Khoj agent"}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileCard;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
'use client'
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
|
@ -8,7 +8,7 @@ import markdownIt from "markdown-it";
|
|||
const md = new markdownIt({
|
||||
html: true,
|
||||
linkify: true,
|
||||
typographer: true
|
||||
typographer: true,
|
||||
});
|
||||
|
||||
import { Context, WebPage, OnlineContext } from "../chatMessage/chatMessage";
|
||||
|
@ -23,7 +23,7 @@ import {
|
|||
SheetTrigger,
|
||||
} from "@/components/ui/sheet";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import DOMPurify from 'dompurify';
|
||||
import DOMPurify from "dompurify";
|
||||
|
||||
interface NotesContextReferenceData {
|
||||
title: string;
|
||||
|
@ -34,43 +34,50 @@ interface NotesContextReferenceCardProps extends NotesContextReferenceData {
|
|||
showFullContent: boolean;
|
||||
}
|
||||
|
||||
|
||||
function NotesContextReferenceCard(props: NotesContextReferenceCardProps) {
|
||||
const snippet = props.showFullContent ? DOMPurify.sanitize(md.render(props.content)) : DOMPurify.sanitize(props.content);
|
||||
const snippet = props.showFullContent
|
||||
? DOMPurify.sanitize(md.render(props.content))
|
||||
: DOMPurify.sanitize(props.content);
|
||||
const [isHovering, setIsHovering] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popover
|
||||
open={isHovering && !props.showFullContent}
|
||||
onOpenChange={setIsHovering}
|
||||
>
|
||||
<Popover open={isHovering && !props.showFullContent} onOpenChange={setIsHovering}>
|
||||
<PopoverTrigger asChild>
|
||||
<Card
|
||||
onMouseEnter={() => setIsHovering(true)}
|
||||
onMouseLeave={() => setIsHovering(false)}
|
||||
className={`${props.showFullContent ? 'w-auto' : 'w-[200px]'} overflow-hidden break-words text-balance rounded-lg p-2 bg-muted border-none`}
|
||||
className={`${props.showFullContent ? "w-auto" : "w-[200px]"} overflow-hidden break-words text-balance rounded-lg p-2 bg-muted border-none`}
|
||||
>
|
||||
<h3 className={`${props.showFullContent ? 'block' : 'line-clamp-1'} text-muted-foreground}`}>
|
||||
<File className='w-6 h-6 text-muted-foreground inline-flex' />
|
||||
<h3
|
||||
className={`${props.showFullContent ? "block" : "line-clamp-1"} text-muted-foreground}`}
|
||||
>
|
||||
<File className="w-6 h-6 text-muted-foreground inline-flex" />
|
||||
{props.title}
|
||||
</h3>
|
||||
<p className={`${props.showFullContent ? 'block' : 'overflow-hidden line-clamp-2'}`} dangerouslySetInnerHTML={{ __html: snippet }}></p>
|
||||
<p
|
||||
className={`${props.showFullContent ? "block" : "overflow-hidden line-clamp-2"}`}
|
||||
dangerouslySetInnerHTML={{ __html: snippet }}
|
||||
></p>
|
||||
</Card>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-[400px] mx-2">
|
||||
<Card className={`w-auto overflow-hidden break-words text-balance rounded-lg p-2 border-none`}>
|
||||
<PopoverContent className="w-[400px] mx-2">
|
||||
<Card
|
||||
className={`w-auto overflow-hidden break-words text-balance rounded-lg p-2 border-none`}
|
||||
>
|
||||
<h3 className={`line-clamp-2 text-muted-foreground}`}>
|
||||
<File className='w-6 h-6 text-muted-foreground inline-flex' />
|
||||
<File className="w-6 h-6 text-muted-foreground inline-flex" />
|
||||
{props.title}
|
||||
</h3>
|
||||
<p className={`overflow-hidden line-clamp-3`} dangerouslySetInnerHTML={{ __html: snippet }}></p>
|
||||
<p
|
||||
className={`overflow-hidden line-clamp-3`}
|
||||
dangerouslySetInnerHTML={{ __html: snippet }}
|
||||
></p>
|
||||
</Card>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export interface ReferencePanelData {
|
||||
|
@ -78,7 +85,6 @@ export interface ReferencePanelData {
|
|||
onlineReferenceCardData: OnlineReferenceData[];
|
||||
}
|
||||
|
||||
|
||||
interface OnlineReferenceData {
|
||||
title: string;
|
||||
description: string;
|
||||
|
@ -92,7 +98,7 @@ interface OnlineReferenceCardProps extends OnlineReferenceData {
|
|||
function GenericOnlineReferenceCard(props: OnlineReferenceCardProps) {
|
||||
const [isHovering, setIsHovering] = useState(false);
|
||||
|
||||
if (!props.link || props.link.split(' ').length > 1) {
|
||||
if (!props.link || props.link.split(" ").length > 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -108,61 +114,89 @@ function GenericOnlineReferenceCard(props: OnlineReferenceCardProps) {
|
|||
|
||||
const handleMouseEnter = () => {
|
||||
setIsHovering(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setIsHovering(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popover
|
||||
open={isHovering && !props.showFullContent}
|
||||
onOpenChange={setIsHovering}
|
||||
>
|
||||
<Popover open={isHovering && !props.showFullContent} onOpenChange={setIsHovering}>
|
||||
<PopoverTrigger asChild>
|
||||
<Card
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
className={`${props.showFullContent ? 'w-auto' : 'w-[200px]'} overflow-hidden break-words rounded-lg text-balance p-2 bg-muted border-none`}>
|
||||
|
||||
<div className='flex flex-col'>
|
||||
<a href={props.link} target="_blank" rel="noreferrer" className='!no-underline p-2'>
|
||||
<div className='flex items-center'>
|
||||
<img src={favicon} alt="" className='!w-4 h-4 mr-2' />
|
||||
<h3 className={`overflow-hidden ${props.showFullContent ? 'block' : 'line-clamp-1'} text-muted-foreground`}>{domain}</h3>
|
||||
className={`${props.showFullContent ? "w-auto" : "w-[200px]"} overflow-hidden break-words rounded-lg text-balance p-2 bg-muted border-none`}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<a
|
||||
href={props.link}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="!no-underline p-2"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<img src={favicon} alt="" className="!w-4 h-4 mr-2" />
|
||||
<h3
|
||||
className={`overflow-hidden ${props.showFullContent ? "block" : "line-clamp-1"} text-muted-foreground`}
|
||||
>
|
||||
{domain}
|
||||
</h3>
|
||||
</div>
|
||||
<h3 className={`overflow-hidden ${props.showFullContent ? 'block' : 'line-clamp-1'} font-bold`}>{props.title}</h3>
|
||||
<p className={`overflow-hidden ${props.showFullContent ? 'block' : 'line-clamp-2'}`}>{props.description}</p>
|
||||
<h3
|
||||
className={`overflow-hidden ${props.showFullContent ? "block" : "line-clamp-1"} font-bold`}
|
||||
>
|
||||
{props.title}
|
||||
</h3>
|
||||
<p
|
||||
className={`overflow-hidden ${props.showFullContent ? "block" : "line-clamp-2"}`}
|
||||
>
|
||||
{props.description}
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
</Card>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-[400px] mx-2">
|
||||
<PopoverContent className="w-[400px] mx-2">
|
||||
<Card
|
||||
className={`w-auto overflow-hidden break-words text-balance rounded-lg border-none`}>
|
||||
|
||||
<div className='flex flex-col'>
|
||||
<a href={props.link} target="_blank" rel="noreferrer" className='!no-underline p-2'>
|
||||
<div className='flex items-center'>
|
||||
<img src={favicon} alt="" className='!w-4 h-4 mr-2' />
|
||||
<h3 className={`overflow-hidden ${props.showFullContent ? 'block' : 'line-clamp-2'} text-muted-foreground`}>{domain}</h3>
|
||||
className={`w-auto overflow-hidden break-words text-balance rounded-lg border-none`}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<a
|
||||
href={props.link}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="!no-underline p-2"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<img src={favicon} alt="" className="!w-4 h-4 mr-2" />
|
||||
<h3
|
||||
className={`overflow-hidden ${props.showFullContent ? "block" : "line-clamp-2"} text-muted-foreground`}
|
||||
>
|
||||
{domain}
|
||||
</h3>
|
||||
</div>
|
||||
<h3 className={`overflow-hidden ${props.showFullContent ? 'block' : 'line-clamp-2'} font-bold`}>{props.title}</h3>
|
||||
<p className={`overflow-hidden ${props.showFullContent ? 'block' : 'line-clamp-3'}`}>{props.description}</p>
|
||||
<h3
|
||||
className={`overflow-hidden ${props.showFullContent ? "block" : "line-clamp-2"} font-bold`}
|
||||
>
|
||||
{props.title}
|
||||
</h3>
|
||||
<p
|
||||
className={`overflow-hidden ${props.showFullContent ? "block" : "line-clamp-3"}`}
|
||||
>
|
||||
{props.description}
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
</Card>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function constructAllReferences(contextData: Context[], onlineData: OnlineContext) {
|
||||
|
||||
const onlineReferences: OnlineReferenceData[] = [];
|
||||
const contextReferences: NotesContextReferenceData[] = [];
|
||||
|
||||
|
@ -173,14 +207,14 @@ export function constructAllReferences(contextData: Context[], onlineData: Onlin
|
|||
localOnlineReferences.push({
|
||||
title: value.answerBox.title,
|
||||
description: value.answerBox.answer,
|
||||
link: value.answerBox.source
|
||||
link: value.answerBox.source,
|
||||
});
|
||||
}
|
||||
if (value.knowledgeGraph) {
|
||||
localOnlineReferences.push({
|
||||
title: value.knowledgeGraph.title,
|
||||
description: value.knowledgeGraph.description,
|
||||
link: value.knowledgeGraph.descriptionLink
|
||||
link: value.knowledgeGraph.descriptionLink,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -191,8 +225,8 @@ export function constructAllReferences(contextData: Context[], onlineData: Onlin
|
|||
return {
|
||||
title: webPage.query,
|
||||
description: webPage.snippet,
|
||||
link: webPage.link
|
||||
}
|
||||
link: webPage.link,
|
||||
};
|
||||
});
|
||||
localOnlineReferences.push(...webPageResults);
|
||||
} else {
|
||||
|
@ -202,7 +236,7 @@ export function constructAllReferences(contextData: Context[], onlineData: Onlin
|
|||
localOnlineReferences.push({
|
||||
title: singleWebpage.query,
|
||||
description: singleWebpage.snippet,
|
||||
link: singleWebpage.link
|
||||
link: singleWebpage.link,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -212,8 +246,8 @@ export function constructAllReferences(contextData: Context[], onlineData: Onlin
|
|||
return {
|
||||
title: organicContext.title,
|
||||
description: organicContext.snippet,
|
||||
link: organicContext.link
|
||||
}
|
||||
link: organicContext.link,
|
||||
};
|
||||
});
|
||||
|
||||
localOnlineReferences.push(...organicResults);
|
||||
|
@ -224,21 +258,20 @@ export function constructAllReferences(contextData: Context[], onlineData: Onlin
|
|||
}
|
||||
|
||||
if (contextData) {
|
||||
|
||||
let localContextReferences = contextData.map((context) => {
|
||||
if (!context.compiled) {
|
||||
const fileContent = context as unknown as string;
|
||||
const title = fileContent.split('\n')[0];
|
||||
const content = fileContent.split('\n').slice(1).join('\n');
|
||||
const title = fileContent.split("\n")[0];
|
||||
const content = fileContent.split("\n").slice(1).join("\n");
|
||||
return {
|
||||
title: title,
|
||||
content: content
|
||||
content: content,
|
||||
};
|
||||
}
|
||||
return {
|
||||
title: context.file,
|
||||
content: context.compiled
|
||||
}
|
||||
content: context.compiled,
|
||||
};
|
||||
});
|
||||
|
||||
contextReferences.push(...localContextReferences);
|
||||
|
@ -246,11 +279,10 @@ export function constructAllReferences(contextData: Context[], onlineData: Onlin
|
|||
|
||||
return {
|
||||
notesReferenceCardData: contextReferences,
|
||||
onlineReferenceCardData: onlineReferences
|
||||
}
|
||||
onlineReferenceCardData: onlineReferences,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export interface TeaserReferenceSectionProps {
|
||||
notesReferenceCardData: NotesContextReferenceData[];
|
||||
onlineReferenceCardData: OnlineReferenceData[];
|
||||
|
@ -265,11 +297,16 @@ export function TeaserReferencesSection(props: TeaserReferenceSectionProps) {
|
|||
}, [props.isMobileWidth]);
|
||||
|
||||
const notesDataToShow = props.notesReferenceCardData.slice(0, numTeaserSlots);
|
||||
const onlineDataToShow = notesDataToShow.length < numTeaserSlots ? props.onlineReferenceCardData.slice(0, numTeaserSlots - notesDataToShow.length) : [];
|
||||
const onlineDataToShow =
|
||||
notesDataToShow.length < numTeaserSlots
|
||||
? props.onlineReferenceCardData.slice(0, numTeaserSlots - notesDataToShow.length)
|
||||
: [];
|
||||
|
||||
const shouldShowShowMoreButton = props.notesReferenceCardData.length > 0 || props.onlineReferenceCardData.length > 0;
|
||||
const shouldShowShowMoreButton =
|
||||
props.notesReferenceCardData.length > 0 || props.onlineReferenceCardData.length > 0;
|
||||
|
||||
const numReferences = props.notesReferenceCardData.length + props.onlineReferenceCardData.length;
|
||||
const numReferences =
|
||||
props.notesReferenceCardData.length + props.onlineReferenceCardData.length;
|
||||
|
||||
if (numReferences === 0) {
|
||||
return null;
|
||||
|
@ -279,50 +316,53 @@ export function TeaserReferencesSection(props: TeaserReferenceSectionProps) {
|
|||
<div className="pt-0 px-4 pb-4 md:px-6">
|
||||
<h3 className="inline-flex items-center">
|
||||
References
|
||||
<p className="text-gray-400 m-2">
|
||||
{numReferences} sources
|
||||
</p>
|
||||
<p className="text-gray-400 m-2">{numReferences} sources</p>
|
||||
</h3>
|
||||
<div className={`flex flex-wrap gap-2 w-auto mt-2`}>
|
||||
{
|
||||
notesDataToShow.map((note, index) => {
|
||||
return <NotesContextReferenceCard showFullContent={false} {...note} key={`${note.title}-${index}`} />
|
||||
})
|
||||
}
|
||||
{
|
||||
onlineDataToShow.map((online, index) => {
|
||||
return <GenericOnlineReferenceCard showFullContent={false} {...online} key={`${online.title}-${index}`} />
|
||||
})
|
||||
}
|
||||
{
|
||||
shouldShowShowMoreButton &&
|
||||
{notesDataToShow.map((note, index) => {
|
||||
return (
|
||||
<NotesContextReferenceCard
|
||||
showFullContent={false}
|
||||
{...note}
|
||||
key={`${note.title}-${index}`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{onlineDataToShow.map((online, index) => {
|
||||
return (
|
||||
<GenericOnlineReferenceCard
|
||||
showFullContent={false}
|
||||
{...online}
|
||||
key={`${online.title}-${index}`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{shouldShowShowMoreButton && (
|
||||
<ReferencePanel
|
||||
notesReferenceCardData={props.notesReferenceCardData}
|
||||
onlineReferenceCardData={props.onlineReferenceCardData} />
|
||||
}
|
||||
onlineReferenceCardData={props.onlineReferenceCardData}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
interface ReferencePanelDataProps {
|
||||
notesReferenceCardData: NotesContextReferenceData[];
|
||||
onlineReferenceCardData: OnlineReferenceData[];
|
||||
}
|
||||
|
||||
export default function ReferencePanel(props: ReferencePanelDataProps) {
|
||||
|
||||
if (!props.notesReferenceCardData && !props.onlineReferenceCardData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Sheet>
|
||||
<SheetTrigger
|
||||
className='text-balance w-auto md:w-[200px] justify-start overflow-hidden break-words p-0 bg-transparent border-none text-gray-400 align-middle items-center !m-2 inline-flex'>
|
||||
<SheetTrigger className="text-balance w-auto md:w-[200px] justify-start overflow-hidden break-words p-0 bg-transparent border-none text-gray-400 align-middle items-center !m-2 inline-flex">
|
||||
View references
|
||||
<ArrowRight className='m-1' />
|
||||
<ArrowRight className="m-1" />
|
||||
</SheetTrigger>
|
||||
<SheetContent className="overflow-y-scroll">
|
||||
<SheetHeader>
|
||||
|
@ -330,16 +370,24 @@ export default function ReferencePanel(props: ReferencePanelDataProps) {
|
|||
<SheetDescription>View all references for this response</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className="flex flex-wrap gap-2 w-auto mt-2">
|
||||
{
|
||||
props.notesReferenceCardData.map((note, index) => {
|
||||
return <NotesContextReferenceCard showFullContent={true} {...note} key={`${note.title}-${index}`} />
|
||||
})
|
||||
}
|
||||
{
|
||||
props.onlineReferenceCardData.map((online, index) => {
|
||||
return <GenericOnlineReferenceCard showFullContent={true} {...online} key={`${online.title}-${index}`} />
|
||||
})
|
||||
}
|
||||
{props.notesReferenceCardData.map((note, index) => {
|
||||
return (
|
||||
<NotesContextReferenceCard
|
||||
showFullContent={true}
|
||||
{...note}
|
||||
key={`${note.title}-${index}`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{props.onlineReferenceCardData.map((online, index) => {
|
||||
return (
|
||||
<GenericOnlineReferenceCard
|
||||
showFullContent={true}
|
||||
{...online}
|
||||
key={`${online.title}-${index}`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
|
|
@ -34,37 +34,34 @@ function copyToClipboard(text: string) {
|
|||
export default function ShareLink(props: ShareLinkProps) {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger
|
||||
asChild
|
||||
onClick={props.onShare}>
|
||||
<Button size="sm" className={`${props.buttonClassName || 'px-3'}`} variant={props.buttonVariant ?? 'default' as const}>
|
||||
{
|
||||
props.includeIcon && (
|
||||
<Share className="w-4 h-4 mr-2" />
|
||||
)
|
||||
}
|
||||
<DialogTrigger asChild onClick={props.onShare}>
|
||||
<Button
|
||||
size="sm"
|
||||
className={`${props.buttonClassName || "px-3"}`}
|
||||
variant={props.buttonVariant ?? ("default" as const)}
|
||||
>
|
||||
{props.includeIcon && <Share className="w-4 h-4 mr-2" />}
|
||||
{props.buttonTitle}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{props.title}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{props.description}
|
||||
</DialogDescription>
|
||||
<DialogDescription>{props.description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="grid flex-1 gap-2">
|
||||
<Label htmlFor="link" className="sr-only">
|
||||
Link
|
||||
</Label>
|
||||
<Input
|
||||
id="link"
|
||||
defaultValue={props.url}
|
||||
readOnly
|
||||
/>
|
||||
<Input id="link" defaultValue={props.url} readOnly />
|
||||
</div>
|
||||
<Button type="submit" size="sm" className="px-3" onClick={() => copyToClipboard(props.url)}>
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
className="px-3"
|
||||
onClick={() => copyToClipboard(props.url)}
|
||||
>
|
||||
<span>Copy</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
'use client'
|
||||
"use client";
|
||||
|
||||
import styles from "./sidePanel.module.css";
|
||||
|
||||
|
@ -40,10 +40,22 @@ import {
|
|||
DrawerTrigger,
|
||||
} from "@/components/ui/drawer";
|
||||
|
||||
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
|
||||
import { ArrowRight, ArrowLeft, ArrowDown, Spinner, Check, FolderPlus, DotsThreeVertical, House, StackPlus, UserCirclePlus, Sidebar, NotePencil } from "@phosphor-icons/react";
|
||||
import {
|
||||
ArrowRight,
|
||||
ArrowLeft,
|
||||
ArrowDown,
|
||||
Spinner,
|
||||
Check,
|
||||
FolderPlus,
|
||||
DotsThreeVertical,
|
||||
House,
|
||||
StackPlus,
|
||||
UserCirclePlus,
|
||||
Sidebar,
|
||||
NotePencil,
|
||||
} from "@phosphor-icons/react";
|
||||
|
||||
interface ChatHistory {
|
||||
conversation_id: string;
|
||||
|
@ -62,17 +74,23 @@ import {
|
|||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
|
||||
import { Pencil, Trash, Share } from "@phosphor-icons/react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { modifyFileFilterForConversation } from "@/app/common/chatFunctions";
|
||||
import { ScrollAreaScrollbar } from "@radix-ui/react-scroll-area";
|
||||
import { KhojLogo, KhojLogoType } from "@/app/components/logo/khogLogo";
|
||||
|
@ -89,15 +107,14 @@ function renameConversation(conversationId: string, newTitle: string) {
|
|||
const editUrl = `/api/chat/title?client=web&conversation_id=${conversationId}&title=${newTitle}`;
|
||||
|
||||
fetch(editUrl, {
|
||||
method: 'PATCH',
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
})
|
||||
.catch(err => {
|
||||
.then((response) => response.json())
|
||||
.then((data) => {})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
return;
|
||||
});
|
||||
|
@ -107,16 +124,16 @@ function shareConversation(conversationId: string, setShareUrl: (url: string) =>
|
|||
const shareUrl = `/api/chat/share?client=web&conversation_id=${conversationId}`;
|
||||
|
||||
fetch(shareUrl, {
|
||||
method: 'POST',
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
setShareUrl(data.url);
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
return;
|
||||
});
|
||||
|
@ -126,15 +143,14 @@ function deleteConversation(conversationId: string) {
|
|||
const deleteUrl = `/api/chat/history?client=web&conversation_id=${conversationId}`;
|
||||
|
||||
fetch(deleteUrl, {
|
||||
method: 'DELETE',
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
})
|
||||
.catch(err => {
|
||||
.then((response) => response.json())
|
||||
.then((data) => {})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
return;
|
||||
});
|
||||
|
@ -148,8 +164,14 @@ interface FilesMenuProps {
|
|||
|
||||
function FilesMenu(props: FilesMenuProps) {
|
||||
// Use SWR to fetch files
|
||||
const { data: files, error } = useSWR<string[]>(props.conversationId ? '/api/content/computer' : null, fetcher);
|
||||
const { data: selectedFiles, error: selectedFilesError } = useSWR(props.conversationId ? `/api/chat/conversation/file-filters/${props.conversationId}` : null, fetcher);
|
||||
const { data: files, error } = useSWR<string[]>(
|
||||
props.conversationId ? "/api/content/computer" : null,
|
||||
fetcher,
|
||||
);
|
||||
const { data: selectedFiles, error: selectedFilesError } = useSWR(
|
||||
props.conversationId ? `/api/chat/conversation/file-filters/${props.conversationId}` : null,
|
||||
fetcher,
|
||||
);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [unfilteredFiles, setUnfilteredFiles] = useState<string[]>([]);
|
||||
const [addedFiles, setAddedFiles] = useState<string[]>([]);
|
||||
|
@ -163,11 +185,12 @@ function FilesMenu(props: FilesMenuProps) {
|
|||
|
||||
if (addedFiles) {
|
||||
console.log("addedFiles in useeffect hook", addedFiles);
|
||||
sortedFiles = addedFiles.concat(sortedFiles.filter((filename: string) => !addedFiles.includes(filename)));
|
||||
sortedFiles = addedFiles.concat(
|
||||
sortedFiles.filter((filename: string) => !addedFiles.includes(filename)),
|
||||
);
|
||||
}
|
||||
|
||||
setUnfilteredFiles(sortedFiles);
|
||||
|
||||
}, [files, addedFiles]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -180,18 +203,22 @@ function FilesMenu(props: FilesMenuProps) {
|
|||
if (selectedFiles) {
|
||||
setAddedFiles(selectedFiles);
|
||||
}
|
||||
|
||||
}, [selectedFiles]);
|
||||
|
||||
const removeAllFiles = () => {
|
||||
modifyFileFilterForConversation(props.conversationId, addedFiles, setAddedFiles, 'remove');
|
||||
}
|
||||
modifyFileFilterForConversation(props.conversationId, addedFiles, setAddedFiles, "remove");
|
||||
};
|
||||
|
||||
const addAllFiles = () => {
|
||||
modifyFileFilterForConversation(props.conversationId, unfilteredFiles, setAddedFiles, 'add');
|
||||
}
|
||||
modifyFileFilterForConversation(
|
||||
props.conversationId,
|
||||
unfilteredFiles,
|
||||
setAddedFiles,
|
||||
"add",
|
||||
);
|
||||
};
|
||||
|
||||
if (!props.conversationId) return (<></>);
|
||||
if (!props.conversationId) return <></>;
|
||||
|
||||
if (error) return <div>Failed to load files</div>;
|
||||
if (selectedFilesError) return <div>Failed to load selected files</div>;
|
||||
|
@ -203,7 +230,9 @@ function FilesMenu(props: FilesMenuProps) {
|
|||
<Command>
|
||||
<CommandInput placeholder="Find file" />
|
||||
<CommandList>
|
||||
<CommandEmpty>No results found. <Link href="/search">Try advanced search</Link>.</CommandEmpty>
|
||||
<CommandEmpty>
|
||||
No results found. <Link href="/search">Try advanced search</Link>.
|
||||
</CommandEmpty>
|
||||
<CommandGroup heading="Quick">
|
||||
<CommandItem
|
||||
onSelect={() => {
|
||||
|
@ -223,36 +252,47 @@ function FilesMenu(props: FilesMenuProps) {
|
|||
</CommandItem>
|
||||
</CommandGroup>
|
||||
<CommandGroup heading="Configure files">
|
||||
{unfilteredFiles.map((filename: string) => (
|
||||
addedFiles && addedFiles.includes(filename) ?
|
||||
{unfilteredFiles.map((filename: string) =>
|
||||
addedFiles && addedFiles.includes(filename) ? (
|
||||
<CommandItem
|
||||
key={filename}
|
||||
value={filename}
|
||||
className="bg-accent text-accent-foreground mb-1"
|
||||
onSelect={(value) => {
|
||||
modifyFileFilterForConversation(props.conversationId, [value], setAddedFiles, 'remove');
|
||||
modifyFileFilterForConversation(
|
||||
props.conversationId,
|
||||
[value],
|
||||
setAddedFiles,
|
||||
"remove",
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Check className="h-4 w-4 mr-2" />
|
||||
<span className="break-all">{filename}</span>
|
||||
</CommandItem>
|
||||
:
|
||||
) : (
|
||||
<CommandItem
|
||||
key={filename}
|
||||
className="mb-1"
|
||||
value={filename}
|
||||
onSelect={(value) => {
|
||||
modifyFileFilterForConversation(props.conversationId, [value], setAddedFiles, 'add');
|
||||
modifyFileFilterForConversation(
|
||||
props.conversationId,
|
||||
[value],
|
||||
setAddedFiles,
|
||||
"add",
|
||||
);
|
||||
}}
|
||||
>
|
||||
<span className="break-all">{filename}</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
),
|
||||
)}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (props.isMobileWidth) {
|
||||
return (
|
||||
|
@ -264,7 +304,9 @@ function FilesMenu(props: FilesMenuProps) {
|
|||
<DrawerContent>
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>Files</DrawerTitle>
|
||||
<DrawerDescription>Manage files for this conversation</DrawerDescription>
|
||||
<DrawerDescription>
|
||||
Manage files for this conversation
|
||||
</DrawerDescription>
|
||||
</DrawerHeader>
|
||||
<div className={`${styles.panelWrapper}`}>
|
||||
<FilesMenuCommandBox />
|
||||
|
@ -282,27 +324,26 @@ function FilesMenu(props: FilesMenuProps) {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Popover
|
||||
open={isOpen}
|
||||
onOpenChange={setIsOpen}>
|
||||
<Popover open={isOpen} onOpenChange={setIsOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<div
|
||||
className="w-auto bg-background border border-muted p-4 drop-shadow-sm rounded-2xl my-8">
|
||||
<div className="w-auto bg-background border border-muted p-4 drop-shadow-sm rounded-2xl my-8">
|
||||
<div className="flex items-center justify-between space-x-4">
|
||||
<h4 className="text-sm font-semibold">
|
||||
Manage Context
|
||||
<p>
|
||||
<span className="text-muted-foreground text-xs">Using {addedFiles.length == 0 ? files.length : addedFiles.length} files</span>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
Using{" "}
|
||||
{addedFiles.length == 0 ? files.length : addedFiles.length}{" "}
|
||||
files
|
||||
</span>
|
||||
</p>
|
||||
</h4>
|
||||
<Button variant="ghost" size="sm" className="w-9 p-0">
|
||||
{
|
||||
isOpen ?
|
||||
<ArrowDown className="h-4 w-4" />
|
||||
:
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
|
||||
}
|
||||
{isOpen ? (
|
||||
<ArrowDown className="h-4 w-4" />
|
||||
) : (
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
)}
|
||||
<span className="sr-only">Toggle</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
@ -313,8 +354,7 @@ function FilesMenu(props: FilesMenuProps) {
|
|||
</PopoverContent>
|
||||
</Popover>
|
||||
</>
|
||||
)
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
interface SessionsAndFilesProps {
|
||||
|
@ -333,38 +373,56 @@ function SessionsAndFiles(props: SessionsAndFilesProps) {
|
|||
<>
|
||||
<div>
|
||||
<ScrollArea>
|
||||
<ScrollAreaScrollbar orientation="vertical" className="h-full w-2.5 border-l border-l-transparent p-[1px]" />
|
||||
<ScrollAreaScrollbar
|
||||
orientation="vertical"
|
||||
className="h-full w-2.5 border-l border-l-transparent p-[1px]"
|
||||
/>
|
||||
<div className={styles.sessionsList}>
|
||||
{props.subsetOrganizedData != null && Object.keys(props.subsetOrganizedData).filter(tg => tg !== "All Time").map((timeGrouping) => (
|
||||
<div key={timeGrouping} className={`my-4`}>
|
||||
<div className={`text-muted-foreground text-sm font-bold p-[0.5rem]`}>
|
||||
{timeGrouping}
|
||||
</div>
|
||||
{props.subsetOrganizedData && props.subsetOrganizedData[timeGrouping].map((chatHistory) => (
|
||||
<ChatSession
|
||||
created={chatHistory.created}
|
||||
compressed={true}
|
||||
key={chatHistory.conversation_id}
|
||||
conversation_id={chatHistory.conversation_id}
|
||||
slug={chatHistory.slug}
|
||||
agent_avatar={chatHistory.agent_avatar}
|
||||
agent_name={chatHistory.agent_name}
|
||||
showSidePanel={props.setEnabled}
|
||||
/>
|
||||
{props.subsetOrganizedData != null &&
|
||||
Object.keys(props.subsetOrganizedData)
|
||||
.filter((tg) => tg !== "All Time")
|
||||
.map((timeGrouping) => (
|
||||
<div key={timeGrouping} className={`my-4`}>
|
||||
<div
|
||||
className={`text-muted-foreground text-sm font-bold p-[0.5rem]`}
|
||||
>
|
||||
{timeGrouping}
|
||||
</div>
|
||||
{props.subsetOrganizedData &&
|
||||
props.subsetOrganizedData[timeGrouping].map(
|
||||
(chatHistory) => (
|
||||
<ChatSession
|
||||
created={chatHistory.created}
|
||||
compressed={true}
|
||||
key={chatHistory.conversation_id}
|
||||
conversation_id={
|
||||
chatHistory.conversation_id
|
||||
}
|
||||
slug={chatHistory.slug}
|
||||
agent_avatar={chatHistory.agent_avatar}
|
||||
agent_name={chatHistory.agent_name}
|
||||
showSidePanel={props.setEnabled}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
{
|
||||
(props.data && props.data.length > 5) && (
|
||||
<ChatSessionsModal data={props.organizedData} showSidePanel={props.setEnabled} />
|
||||
)
|
||||
}
|
||||
{props.data && props.data.length > 5 && (
|
||||
<ChatSessionsModal
|
||||
data={props.organizedData}
|
||||
showSidePanel={props.setEnabled}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<FilesMenu conversationId={props.conversationId} uploadedFiles={props.uploadedFiles} isMobileWidth={props.isMobileWidth} />
|
||||
<FilesMenu
|
||||
conversationId={props.conversationId}
|
||||
uploadedFiles={props.uploadedFiles}
|
||||
isMobileWidth={props.isMobileWidth}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
interface ChatSessionActionMenuProps {
|
||||
|
@ -373,11 +431,11 @@ interface ChatSessionActionMenuProps {
|
|||
}
|
||||
|
||||
function ChatSessionActionMenu(props: ChatSessionActionMenuProps) {
|
||||
const [renamedTitle, setRenamedTitle] = useState('');
|
||||
const [renamedTitle, setRenamedTitle] = useState("");
|
||||
const [isRenaming, setIsRenaming] = useState(false);
|
||||
const [isSharing, setIsSharing] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [shareUrl, setShareUrl] = useState('');
|
||||
const [shareUrl, setShareUrl] = useState("");
|
||||
const [showShareUrl, setShowShareUrl] = useState(false);
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
@ -392,14 +450,13 @@ function ChatSessionActionMenu(props: ChatSessionActionMenuProps) {
|
|||
|
||||
if (isRenaming) {
|
||||
return (
|
||||
<Dialog
|
||||
open={isRenaming}
|
||||
onOpenChange={(open) => setIsRenaming(open)}>
|
||||
<Dialog open={isRenaming} onOpenChange={(open) => setIsRenaming(open)}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Set a new title for the conversation</DialogTitle>
|
||||
<DialogDescription>
|
||||
This will help you identify the conversation easily, and also help you search for it later.
|
||||
This will help you identify the conversation easily, and also help you
|
||||
search for it later.
|
||||
</DialogDescription>
|
||||
<Input
|
||||
value={renamedTitle}
|
||||
|
@ -413,11 +470,14 @@ function ChatSessionActionMenu(props: ChatSessionActionMenuProps) {
|
|||
props.setTitle(renamedTitle);
|
||||
setIsRenaming(false);
|
||||
}}
|
||||
type="submit">Rename</Button>
|
||||
type="submit"
|
||||
>
|
||||
Rename
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (isSharing || showShareUrl) {
|
||||
|
@ -428,14 +488,16 @@ function ChatSessionActionMenu(props: ChatSessionActionMenuProps) {
|
|||
<Dialog
|
||||
open={isSharing || showShareUrl}
|
||||
onOpenChange={(open) => {
|
||||
setShowShareUrl(open)
|
||||
setIsSharing(open)
|
||||
}}>
|
||||
setShowShareUrl(open);
|
||||
setIsSharing(open);
|
||||
}}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Conversation Share URL</DialogTitle>
|
||||
<DialogDescription>
|
||||
Sharing this chat session will allow anyone with a link to view the conversation.
|
||||
Sharing this chat session will allow anyone with a link to view the
|
||||
conversation.
|
||||
<Input
|
||||
className="w-full bg-accent text-accent-foreground rounded-md p-2 mt-2"
|
||||
value={shareUrl}
|
||||
|
@ -444,40 +506,44 @@ function ChatSessionActionMenu(props: ChatSessionActionMenuProps) {
|
|||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
{
|
||||
!showShareUrl &&
|
||||
{!showShareUrl && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
shareConversation(props.conversationId, setShareUrl);
|
||||
setShowShareUrl(true);
|
||||
}}
|
||||
className="bg-orange-500"
|
||||
disabled><Spinner className="mr-2 h-4 w-4 animate-spin" />Sharing</Button>
|
||||
}
|
||||
{
|
||||
showShareUrl &&
|
||||
disabled
|
||||
>
|
||||
<Spinner className="mr-2 h-4 w-4 animate-spin" />
|
||||
Sharing
|
||||
</Button>
|
||||
)}
|
||||
{showShareUrl && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(shareUrl);
|
||||
}}
|
||||
variant={'default'}>Copy</Button>
|
||||
}
|
||||
variant={"default"}
|
||||
>
|
||||
Copy
|
||||
</Button>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (isDeleting) {
|
||||
return (
|
||||
<AlertDialog
|
||||
open={isDeleting}
|
||||
onOpenChange={(open) => setIsDeleting(open)}>
|
||||
<AlertDialog open={isDeleting} onOpenChange={(open) => setIsDeleting(open)}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete Conversation</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to delete this conversation? This action cannot be undone.
|
||||
Are you sure you want to delete this conversation? This action cannot be
|
||||
undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
|
@ -486,60 +552,88 @@ function ChatSessionActionMenu(props: ChatSessionActionMenuProps) {
|
|||
onClick={() => {
|
||||
deleteConversation(props.conversationId);
|
||||
setIsDeleting(false);
|
||||
var currConversationId = parseInt(new URLSearchParams(window.location.search).get('conversationId') || "0");
|
||||
var currConversationId = parseInt(
|
||||
new URLSearchParams(window.location.search).get(
|
||||
"conversationId",
|
||||
) || "0",
|
||||
);
|
||||
//edge case for deleting current conversation
|
||||
if (currConversationId === parseInt(props.conversationId)) {
|
||||
window.location.href = '/';
|
||||
window.location.href = "/";
|
||||
}
|
||||
//reload side panel
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
}}
|
||||
className="bg-rose-500 hover:bg-rose-600">Delete</AlertDialogAction>
|
||||
className="bg-rose-500 hover:bg-rose-600"
|
||||
>
|
||||
Delete
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu
|
||||
onOpenChange={(open) => setIsOpen(open)}
|
||||
open={isOpen}>
|
||||
<DropdownMenuTrigger><DotsThreeVertical className="h-4 w-4" /></DropdownMenuTrigger>
|
||||
<DropdownMenu onOpenChange={(open) => setIsOpen(open)} open={isOpen}>
|
||||
<DropdownMenuTrigger>
|
||||
<DotsThreeVertical className="h-4 w-4" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>
|
||||
<Button className="p-0 text-sm h-auto" variant={'ghost'} onClick={() => setIsRenaming(true)}>
|
||||
<Pencil className="mr-2 h-4 w-4" />Rename
|
||||
<Button
|
||||
className="p-0 text-sm h-auto"
|
||||
variant={"ghost"}
|
||||
onClick={() => setIsRenaming(true)}
|
||||
>
|
||||
<Pencil className="mr-2 h-4 w-4" />
|
||||
Rename
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Button className="p-0 text-sm h-auto" variant={'ghost'} onClick={() => setIsSharing(true)}>
|
||||
<Share className="mr-2 h-4 w-4" />Share
|
||||
<Button
|
||||
className="p-0 text-sm h-auto"
|
||||
variant={"ghost"}
|
||||
onClick={() => setIsSharing(true)}
|
||||
>
|
||||
<Share className="mr-2 h-4 w-4" />
|
||||
Share
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Button className="p-0 text-sm h-auto text-rose-300 hover:text-rose-400" variant={'ghost'} onClick={() => setIsDeleting(true)}>
|
||||
<Trash className="mr-2 h-4 w-4" />Delete
|
||||
<Button
|
||||
className="p-0 text-sm h-auto text-rose-300 hover:text-rose-400"
|
||||
variant={"ghost"}
|
||||
onClick={() => setIsDeleting(true)}
|
||||
>
|
||||
<Trash className="mr-2 h-4 w-4" />
|
||||
Delete
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ChatSession(props: ChatHistory) {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const [title, setTitle] = useState(props.slug || "New Conversation 🌱");
|
||||
var currConversationId = parseInt(new URLSearchParams(window.location.search).get('conversationId') || "-1");
|
||||
var currConversationId = parseInt(
|
||||
new URLSearchParams(window.location.search).get("conversationId") || "-1",
|
||||
);
|
||||
return (
|
||||
<div
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
key={props.conversation_id}
|
||||
className={`${styles.session} ${props.compressed ? styles.compressed : '!max-w-full'} ${isHovered ? `${styles.sessionHover}` : ''} ${currConversationId === parseInt(props.conversation_id) && currConversationId != -1 ? "dark:bg-neutral-800 bg-white" : ""}`}>
|
||||
<Link href={`/chat?conversationId=${props.conversation_id}`} onClick={() => props.showSidePanel(false)}>
|
||||
className={`${styles.session} ${props.compressed ? styles.compressed : "!max-w-full"} ${isHovered ? `${styles.sessionHover}` : ""} ${currConversationId === parseInt(props.conversation_id) && currConversationId != -1 ? "dark:bg-neutral-800 bg-white" : ""}`}
|
||||
>
|
||||
<Link
|
||||
href={`/chat?conversationId=${props.conversation_id}`}
|
||||
onClick={() => props.showSidePanel(false)}
|
||||
>
|
||||
<p className={styles.session}>{title}</p>
|
||||
</Link>
|
||||
<ChatSessionActionMenu conversationId={props.conversation_id} setTitle={setTitle} />
|
||||
|
@ -555,34 +649,38 @@ interface ChatSessionsModalProps {
|
|||
function ChatSessionsModal({ data, showSidePanel }: ChatSessionsModalProps) {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger
|
||||
className="flex text-left text-medium text-gray-500 hover:text-gray-300 cursor-pointer my-4 text-sm p-[0.5rem]">
|
||||
<span className="mr-2">See All <ArrowRight className="inline h-4 w-4" weight="bold" /></span>
|
||||
<DialogTrigger className="flex text-left text-medium text-gray-500 hover:text-gray-300 cursor-pointer my-4 text-sm p-[0.5rem]">
|
||||
<span className="mr-2">
|
||||
See All <ArrowRight className="inline h-4 w-4" weight="bold" />
|
||||
</span>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>All Conversations</DialogTitle>
|
||||
<DialogDescription
|
||||
className="p-0">
|
||||
<DialogDescription className="p-0">
|
||||
<ScrollArea className="h-[500px] py-4">
|
||||
{data && Object.keys(data).map((timeGrouping) => (
|
||||
<div key={timeGrouping}>
|
||||
<div className={`text-muted-foreground text-sm font-bold p-[0.5rem] `}>
|
||||
{timeGrouping}
|
||||
{data &&
|
||||
Object.keys(data).map((timeGrouping) => (
|
||||
<div key={timeGrouping}>
|
||||
<div
|
||||
className={`text-muted-foreground text-sm font-bold p-[0.5rem] `}
|
||||
>
|
||||
{timeGrouping}
|
||||
</div>
|
||||
{data[timeGrouping].map((chatHistory) => (
|
||||
<ChatSession
|
||||
created={chatHistory.created}
|
||||
compressed={false}
|
||||
key={chatHistory.conversation_id}
|
||||
conversation_id={chatHistory.conversation_id}
|
||||
slug={chatHistory.slug}
|
||||
agent_avatar={chatHistory.agent_avatar}
|
||||
agent_name={chatHistory.agent_name}
|
||||
showSidePanel={showSidePanel}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{data[timeGrouping].map((chatHistory) => (
|
||||
<ChatSession
|
||||
created={chatHistory.created}
|
||||
compressed={false}
|
||||
key={chatHistory.conversation_id}
|
||||
conversation_id={chatHistory.conversation_id}
|
||||
slug={chatHistory.slug}
|
||||
agent_avatar={chatHistory.agent_avatar}
|
||||
agent_name={chatHistory.agent_name}
|
||||
showSidePanel={showSidePanel} />
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
))}
|
||||
</ScrollArea>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
@ -593,9 +691,9 @@ function ChatSessionsModal({ data, showSidePanel }: ChatSessionsModalProps) {
|
|||
|
||||
const fetchChatHistory = async (url: string) => {
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
method: "GET",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
return response.json();
|
||||
|
@ -617,7 +715,6 @@ interface SidePanelProps {
|
|||
isMobileWidth: boolean;
|
||||
}
|
||||
|
||||
|
||||
export default function SidePanel(props: SidePanelProps) {
|
||||
const [data, setData] = useState<ChatHistory[] | null>(null);
|
||||
const [organizedData, setOrganizedData] = useState<GroupedChatHistory | null>(null);
|
||||
|
@ -625,7 +722,9 @@ export default function SidePanel(props: SidePanelProps) {
|
|||
const [enabled, setEnabled] = useState(false);
|
||||
|
||||
const authenticatedData = useAuthenticatedData();
|
||||
const { data: chatSessions } = useChatSessionsFetchRequest(authenticatedData ? `/api/chat/sessions` : '');
|
||||
const { data: chatSessions } = useChatSessionsFetchRequest(
|
||||
authenticatedData ? `/api/chat/sessions` : "",
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (chatSessions) {
|
||||
|
@ -642,7 +741,8 @@ export default function SidePanel(props: SidePanelProps) {
|
|||
const diffTime = Math.abs(currentDate.getTime() - chatDate.getTime());
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
|
||||
const timeGrouping = diffDays < 7 ? 'Recent' : diffDays < 30 ? 'Last Month' : 'All Time';
|
||||
const timeGrouping =
|
||||
diffDays < 7 ? "Recent" : diffDays < 30 ? "Last Month" : "All Time";
|
||||
if (!groupedData[timeGrouping]) {
|
||||
groupedData[timeGrouping] = [];
|
||||
}
|
||||
|
@ -664,80 +764,98 @@ export default function SidePanel(props: SidePanelProps) {
|
|||
}, [chatSessions]);
|
||||
|
||||
return (
|
||||
<div className={`${styles.panel} ${enabled ? styles.expanded : styles.collapsed} ${props.isMobileWidth ? 'mt-0' : 'mt-1'}`}>
|
||||
<div
|
||||
className={`${styles.panel} ${enabled ? styles.expanded : styles.collapsed} ${props.isMobileWidth ? "mt-0" : "mt-1"}`}
|
||||
>
|
||||
<div className={`flex justify-between flex-row`}>
|
||||
{
|
||||
props.isMobileWidth ?
|
||||
<Drawer open={enabled} onOpenChange={(open) => {
|
||||
{props.isMobileWidth ? (
|
||||
<Drawer
|
||||
open={enabled}
|
||||
onOpenChange={(open) => {
|
||||
if (!enabled) setEnabled(false);
|
||||
setEnabled(open);
|
||||
}
|
||||
}>
|
||||
<DrawerTrigger><Sidebar className="h-8 w-8 mx-2" weight="thin" /></DrawerTrigger>
|
||||
<DrawerContent>
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>Sessions and Files</DrawerTitle>
|
||||
<DrawerDescription>View all conversation sessions and manage conversation file filters</DrawerDescription>
|
||||
</DrawerHeader>
|
||||
{
|
||||
authenticatedData ?
|
||||
<div className={`${styles.panelWrapper}`}>
|
||||
<SessionsAndFiles
|
||||
setEnabled={setEnabled}
|
||||
subsetOrganizedData={subsetOrganizedData}
|
||||
organizedData={organizedData}
|
||||
data={data}
|
||||
uploadedFiles={props.uploadedFiles}
|
||||
userProfile={authenticatedData}
|
||||
conversationId={props.conversationId}
|
||||
isMobileWidth={props.isMobileWidth}
|
||||
/>
|
||||
</div>
|
||||
:
|
||||
<div className={`${styles.panelWrapper}`}>
|
||||
<Link href={`/login?next=${encodeURIComponent(window.location.pathname)}`} className="text-center"> {/* Redirect to login page */}
|
||||
<Button variant="default"><UserCirclePlus className="h-4 w-4 mr-1" />Sign Up</Button>
|
||||
</Link>
|
||||
</div>
|
||||
}
|
||||
<DrawerFooter>
|
||||
<DrawerClose>
|
||||
<Button variant="outline">Done</Button>
|
||||
</DrawerClose>
|
||||
</DrawerFooter>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
:
|
||||
<div className={`grid grid-flow-col gap-4 w-fit`}>
|
||||
<Link href='/' className="content-center">
|
||||
<KhojLogoType />
|
||||
}}
|
||||
>
|
||||
<DrawerTrigger>
|
||||
<Sidebar className="h-8 w-8 mx-2" weight="thin" />
|
||||
</DrawerTrigger>
|
||||
<DrawerContent>
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>Sessions and Files</DrawerTitle>
|
||||
<DrawerDescription>
|
||||
View all conversation sessions and manage conversation file
|
||||
filters
|
||||
</DrawerDescription>
|
||||
</DrawerHeader>
|
||||
{authenticatedData ? (
|
||||
<div className={`${styles.panelWrapper}`}>
|
||||
<SessionsAndFiles
|
||||
setEnabled={setEnabled}
|
||||
subsetOrganizedData={subsetOrganizedData}
|
||||
organizedData={organizedData}
|
||||
data={data}
|
||||
uploadedFiles={props.uploadedFiles}
|
||||
userProfile={authenticatedData}
|
||||
conversationId={props.conversationId}
|
||||
isMobileWidth={props.isMobileWidth}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className={`${styles.panelWrapper}`}>
|
||||
<Link
|
||||
href={`/login?next=${encodeURIComponent(window.location.pathname)}`}
|
||||
className="text-center"
|
||||
>
|
||||
{" "}
|
||||
{/* Redirect to login page */}
|
||||
<Button variant="default">
|
||||
<UserCirclePlus className="h-4 w-4 mr-1" />
|
||||
Sign Up
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
<DrawerFooter>
|
||||
<DrawerClose>
|
||||
<Button variant="outline">Done</Button>
|
||||
</DrawerClose>
|
||||
</DrawerFooter>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
) : (
|
||||
<div className={`grid grid-flow-col gap-4 w-fit`}>
|
||||
<Link href="/" className="content-center">
|
||||
<KhojLogoType />
|
||||
</Link>
|
||||
<div className="grid grid-flow-col gap-2 items-center">
|
||||
<Link className="mx-4" href="/">
|
||||
{enabled ? (
|
||||
<NotePencil className="h-6 w-6" weight="fill" />
|
||||
) : (
|
||||
<NotePencil className="h-6 w-6" color="gray" />
|
||||
)}
|
||||
</Link>
|
||||
<div className='grid grid-flow-col gap-2 items-center'>
|
||||
<Link className='mx-4' href="/">
|
||||
{enabled ? <NotePencil className="h-6 w-6" weight="fill" /> : <NotePencil className="h-6 w-6" color="gray" />}
|
||||
</Link>
|
||||
<button className={styles.button} onClick={() => setEnabled(!enabled)}>
|
||||
{enabled ? <Sidebar className="h-6 w-6" weight="fill" /> : <Sidebar className="h-6 w-6" color="gray" />}
|
||||
</button>
|
||||
</div>
|
||||
<div className="fixed right-0 top-[0.9rem] w-fit h-fit">
|
||||
<NavMenu />
|
||||
</div>
|
||||
<button className={styles.button} onClick={() => setEnabled(!enabled)}>
|
||||
{enabled ? (
|
||||
<Sidebar className="h-6 w-6" weight="fill" />
|
||||
) : (
|
||||
<Sidebar className="h-6 w-6" color="gray" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
{
|
||||
props.isMobileWidth &&
|
||||
<Link href='/' className="content-center">
|
||||
<div className="fixed right-0 top-[0.9rem] w-fit h-fit">
|
||||
<NavMenu />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{props.isMobileWidth && (
|
||||
<Link href="/" className="content-center">
|
||||
<KhojLogoType />
|
||||
</Link>
|
||||
}
|
||||
{
|
||||
props.isMobileWidth &&
|
||||
<NavMenu />
|
||||
}
|
||||
)}
|
||||
{props.isMobileWidth && <NavMenu />}
|
||||
</div>
|
||||
{
|
||||
authenticatedData && !props.isMobileWidth && enabled &&
|
||||
{authenticatedData && !props.isMobileWidth && enabled && (
|
||||
<div className={`${styles.panelWrapper}`}>
|
||||
<SessionsAndFiles
|
||||
setEnabled={setEnabled}
|
||||
|
@ -750,19 +868,29 @@ export default function SidePanel(props: SidePanelProps) {
|
|||
isMobileWidth={props.isMobileWidth}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
{
|
||||
!authenticatedData && enabled && !props.isMobileWidth &&
|
||||
)}
|
||||
{!authenticatedData && enabled && !props.isMobileWidth && (
|
||||
<div className={`${styles.panelWrapper}`}>
|
||||
<Link href="/" className="flex flex-col content-start items-start no-underline">
|
||||
<Button variant="ghost"><House className="h-4 w-4 mr-1" />Home</Button>
|
||||
<Button variant="ghost"><StackPlus className="h-4 w-4 mr-1" />New Conversation</Button>
|
||||
<Button variant="ghost">
|
||||
<House className="h-4 w-4 mr-1" />
|
||||
Home
|
||||
</Button>
|
||||
<Button variant="ghost">
|
||||
<StackPlus className="h-4 w-4 mr-1" />
|
||||
New Conversation
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href={`/login?next=${encodeURIComponent(window.location.pathname)}`}> {/* Redirect to login page */}
|
||||
<Button variant="default"><UserCirclePlus className="h-4 w-4 mr-1" />Sign Up</Button>
|
||||
<Link href={`/login?next=${encodeURIComponent(window.location.pathname)}`}>
|
||||
{" "}
|
||||
{/* Redirect to login page */}
|
||||
<Button variant="default">
|
||||
<UserCirclePlus className="h-4 w-4 mr-1" />
|
||||
Sign Up
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,34 +1,25 @@
|
|||
'use client'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card"
|
||||
"use client";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
|
||||
import styles from "./suggestions.module.css";
|
||||
import { getIconFromIconName } from "@/app/common/iconUtils";
|
||||
import { converColorToBgGradient } from "@/app/common/colorUtils";
|
||||
|
||||
|
||||
function convertSuggestionTitleToIconClass(title: string, color: string) {
|
||||
if (title.includes('automation')) return getIconFromIconName('Robot', color, 'w-8', 'h-8');
|
||||
if (title.includes('online')) return getIconFromIconName('Globe', color, 'w-8', 'h-8');
|
||||
if (title.includes('paint')) return getIconFromIconName('Palette', color, 'w-8', 'h-8');
|
||||
if (title.includes('pop')) return getIconFromIconName('Confetti', color, 'w-8', 'h-8');
|
||||
if (title.includes('travel')) return getIconFromIconName('Jeep', color, 'w-8', 'h-8');
|
||||
if (title.includes('learn')) return getIconFromIconName('Book', color, 'w-8', 'h-8');
|
||||
if (title.includes('health')) return getIconFromIconName('Asclepius', color, 'w-8', 'h-8');
|
||||
if (title.includes('fun')) return getIconFromIconName('Island', color, 'w-8', 'h-8');
|
||||
if (title.includes('home')) return getIconFromIconName('House', color, 'w-8', 'h-8');
|
||||
if (title.includes('language')) return getIconFromIconName('Translate', color, 'w-8', 'h-8');
|
||||
if (title.includes('code')) return getIconFromIconName('Code', color, 'w-8', 'h-8');
|
||||
|
||||
else return getIconFromIconName('Lightbulb', color, 'w-8', 'h-8');
|
||||
if (title.includes("automation")) return getIconFromIconName("Robot", color, "w-8", "h-8");
|
||||
if (title.includes("online")) return getIconFromIconName("Globe", color, "w-8", "h-8");
|
||||
if (title.includes("paint")) return getIconFromIconName("Palette", color, "w-8", "h-8");
|
||||
if (title.includes("pop")) return getIconFromIconName("Confetti", color, "w-8", "h-8");
|
||||
if (title.includes("travel")) return getIconFromIconName("Jeep", color, "w-8", "h-8");
|
||||
if (title.includes("learn")) return getIconFromIconName("Book", color, "w-8", "h-8");
|
||||
if (title.includes("health")) return getIconFromIconName("Asclepius", color, "w-8", "h-8");
|
||||
if (title.includes("fun")) return getIconFromIconName("Island", color, "w-8", "h-8");
|
||||
if (title.includes("home")) return getIconFromIconName("House", color, "w-8", "h-8");
|
||||
if (title.includes("language")) return getIconFromIconName("Translate", color, "w-8", "h-8");
|
||||
if (title.includes("code")) return getIconFromIconName("Code", color, "w-8", "h-8");
|
||||
else return getIconFromIconName("Lightbulb", color, "w-8", "h-8");
|
||||
}
|
||||
|
||||
|
||||
interface SuggestionCardProps {
|
||||
title: string;
|
||||
body: string;
|
||||
|
@ -46,12 +37,17 @@ export default function SuggestionCard(data: SuggestionCardProps) {
|
|||
<Card className={cardClassName}>
|
||||
<CardHeader className="m-0 p-2 pb-1 relative">
|
||||
<div className="flex flex-row md:flex-col">
|
||||
{convertSuggestionTitleToIconClass(data.title.toLowerCase(), data.color.toLowerCase())}
|
||||
{convertSuggestionTitleToIconClass(
|
||||
data.title.toLowerCase(),
|
||||
data.color.toLowerCase(),
|
||||
)}
|
||||
<CardTitle className={titleClassName}>{data.title}</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="m-0 p-2 pr-4 pt-1">
|
||||
<CardDescription className={`${descriptionClassName} sm:line-clamp-2 md:line-clamp-4`}>
|
||||
<CardDescription
|
||||
className={`${descriptionClassName} sm:line-clamp-2 md:line-clamp-4`}
|
||||
>
|
||||
{data.body}
|
||||
</CardDescription>
|
||||
</CardContent>
|
||||
|
@ -62,5 +58,7 @@ export default function SuggestionCard(data: SuggestionCardProps) {
|
|||
<a href={data.link} className="no-underline">
|
||||
{cardContent}
|
||||
</a>
|
||||
) : cardContent;
|
||||
) : (
|
||||
cardContent
|
||||
);
|
||||
}
|
||||
|
|
|
@ -149,7 +149,8 @@ export const suggestionsData: Suggestion[] = [
|
|||
{
|
||||
type: "Learning",
|
||||
color: "yellow",
|
||||
description: "Summarize the biography of this figure: https://en.wikipedia.org/wiki/Jean_Baptiste_Point_du_Sable",
|
||||
description:
|
||||
"Summarize the biography of this figure: https://en.wikipedia.org/wiki/Jean_Baptiste_Point_du_Sable",
|
||||
link: "",
|
||||
},
|
||||
{
|
||||
|
@ -179,7 +180,8 @@ export const suggestionsData: Suggestion[] = [
|
|||
{
|
||||
type: "Paint",
|
||||
color: "green",
|
||||
description: "Paint a colorful scene of a traditional Day of the Dead celebration in Mexico.",
|
||||
description:
|
||||
"Paint a colorful scene of a traditional Day of the Dead celebration in Mexico.",
|
||||
link: "",
|
||||
},
|
||||
{
|
||||
|
@ -233,7 +235,8 @@ export const suggestionsData: Suggestion[] = [
|
|||
{
|
||||
type: "Paint",
|
||||
color: "green",
|
||||
description: "Create a digital painting in the style of traditional Chinese ink wash landscape.",
|
||||
description:
|
||||
"Create a digital painting in the style of traditional Chinese ink wash landscape.",
|
||||
link: "",
|
||||
},
|
||||
{
|
||||
|
@ -341,7 +344,8 @@ export const suggestionsData: Suggestion[] = [
|
|||
{
|
||||
type: "Code",
|
||||
color: "purple",
|
||||
description: "Provide a coding challenge to reverse a string without using built-in functions.",
|
||||
description:
|
||||
"Provide a coding challenge to reverse a string without using built-in functions.",
|
||||
link: "",
|
||||
},
|
||||
{
|
||||
|
@ -353,7 +357,8 @@ export const suggestionsData: Suggestion[] = [
|
|||
{
|
||||
type: "Code",
|
||||
color: "purple",
|
||||
description: "Create a coding exercise to implement a basic sorting algorithm (e.g., bubble sort).",
|
||||
description:
|
||||
"Create a coding exercise to implement a basic sorting algorithm (e.g., bubble sort).",
|
||||
link: "",
|
||||
},
|
||||
{
|
||||
|
@ -365,31 +370,36 @@ export const suggestionsData: Suggestion[] = [
|
|||
{
|
||||
type: "Code",
|
||||
color: "purple",
|
||||
description: "Provide a coding challenge to find the longest palindromic substring in a given string.",
|
||||
description:
|
||||
"Provide a coding challenge to find the longest palindromic substring in a given string.",
|
||||
link: "",
|
||||
},
|
||||
{
|
||||
type: "Code",
|
||||
color: "purple",
|
||||
description: "Explain the concept of asynchronous programming with a JavaScript Promise example.",
|
||||
description:
|
||||
"Explain the concept of asynchronous programming with a JavaScript Promise example.",
|
||||
link: "",
|
||||
},
|
||||
{
|
||||
type: "Code",
|
||||
color: "purple",
|
||||
description: "Create a coding exercise to implement a basic data structure (e.g., linked list or stack).",
|
||||
description:
|
||||
"Create a coding exercise to implement a basic data structure (e.g., linked list or stack).",
|
||||
link: "",
|
||||
},
|
||||
{
|
||||
type: "Code",
|
||||
color: "purple",
|
||||
description: "Explain the time and space complexity of common algorithms (e.g., binary search).",
|
||||
description:
|
||||
"Explain the time and space complexity of common algorithms (e.g., binary search).",
|
||||
link: "",
|
||||
},
|
||||
{
|
||||
type: "Code",
|
||||
color: "purple",
|
||||
description: "Provide a coding challenge to implement a simple REST API using Node.js and Express.",
|
||||
description:
|
||||
"Provide a coding challenge to implement a simple REST API using Node.js and Express.",
|
||||
link: "",
|
||||
},
|
||||
];
|
||||
|
|
|
@ -2,9 +2,10 @@ import type { Metadata } from "next";
|
|||
|
||||
export const metadata: Metadata = {
|
||||
title: "Khoj AI - Fact Checker",
|
||||
description: "Use the Fact Checker with Khoj AI for verifying statements. It can research the internet for you, either refuting or confirming the statement using fresh data.",
|
||||
description:
|
||||
"Use the Fact Checker with Khoj AI for verifying statements. It can research the internet for you, either refuting or confirming the statement using fresh data.",
|
||||
icons: {
|
||||
icon: '/static/favicon.ico',
|
||||
icon: "/static/favicon.ico",
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -13,9 +14,5 @@ export default function RootLayout({
|
|||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
return <div>{children}</div>;
|
||||
}
|
||||
|
|
|
@ -1,31 +1,28 @@
|
|||
'use client'
|
||||
"use client";
|
||||
|
||||
import styles from './factChecker.module.css';
|
||||
import { useAuthenticatedData } from '@/app/common/auth';
|
||||
import { useState, useEffect } from 'react';
|
||||
import styles from "./factChecker.module.css";
|
||||
import { useAuthenticatedData } from "@/app/common/auth";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
import ChatMessage, { Context, OnlineContext, OnlineContextData, WebPage } from '../components/chatMessage/chatMessage';
|
||||
import { ModelPicker, Model } from '../components/modelPicker/modelPicker';
|
||||
import ShareLink from '../components/shareLink/shareLink';
|
||||
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Button } from "@/components/ui/button"
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card"
|
||||
import Link from 'next/link';
|
||||
import SidePanel from '../components/sidePanel/chatHistorySidePanel';
|
||||
import ChatMessage, {
|
||||
Context,
|
||||
OnlineContext,
|
||||
OnlineContextData,
|
||||
WebPage,
|
||||
} from "../components/chatMessage/chatMessage";
|
||||
import { ModelPicker, Model } from "../components/modelPicker/modelPicker";
|
||||
import ShareLink from "../components/shareLink/shareLink";
|
||||
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import Link from "next/link";
|
||||
import SidePanel from "../components/sidePanel/chatHistorySidePanel";
|
||||
|
||||
const chatURL = "/api/chat";
|
||||
const verificationPrecursor = "Limit your search to reputable sources. Search the internet for relevant supporting or refuting information. Do not reference my notes. Refuse to answer any queries that are not falsifiable by informing me that you will not answer the question. You're not permitted to ask follow-up questions, so do the best with what you have. Respond with **TRUE** or **FALSE** or **INCONCLUSIVE**, then provide your justification. Fact Check:"
|
||||
|
||||
const verificationPrecursor =
|
||||
"Limit your search to reputable sources. Search the internet for relevant supporting or refuting information. Do not reference my notes. Refuse to answer any queries that are not falsifiable by informing me that you will not answer the question. You're not permitted to ask follow-up questions, so do the best with what you have. Respond with **TRUE** or **FALSE** or **INCONCLUSIVE**, then provide your justification. Fact Check:";
|
||||
|
||||
const LoadingSpinner = () => (
|
||||
<div className={styles.loading}>
|
||||
|
@ -45,7 +42,6 @@ interface SupplementReferences {
|
|||
linkTitle: string;
|
||||
}
|
||||
|
||||
|
||||
interface ResponseWithReferences {
|
||||
context?: Context[];
|
||||
online?: OnlineContext;
|
||||
|
@ -70,13 +66,13 @@ function handleCompiledReferences(chunk: string, currentResponse: string) {
|
|||
return references;
|
||||
}
|
||||
|
||||
|
||||
async function verifyStatement(
|
||||
message: string,
|
||||
conversationId: string,
|
||||
setIsLoading: (loading: boolean) => void,
|
||||
setInitialResponse: (response: string) => void,
|
||||
setInitialReferences: (references: ResponseWithReferences) => void) {
|
||||
setInitialReferences: (references: ResponseWithReferences) => void,
|
||||
) {
|
||||
setIsLoading(true);
|
||||
// Send a message to the chat server to verify the fact
|
||||
let verificationMessage = `${verificationPrecursor} ${message}`;
|
||||
|
@ -114,9 +110,7 @@ async function verifyStatement(
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
async function spawnNewConversation(setConversationID: (conversationID: string) => void) {
|
||||
|
||||
let createURL = `/api/chat/sessions?client=web`;
|
||||
|
||||
const response = await fetch(createURL, { method: "POST" });
|
||||
|
@ -125,13 +119,16 @@ async function spawnNewConversation(setConversationID: (conversationID: string)
|
|||
setConversationID(data.conversation_id);
|
||||
}
|
||||
|
||||
|
||||
interface ReferenceVerificationProps {
|
||||
message: string;
|
||||
additionalLink: string;
|
||||
conversationId: string;
|
||||
linkTitle: string;
|
||||
setChildReferencesCallback: (additionalLink: string, response: string, linkTitle: string) => void;
|
||||
setChildReferencesCallback: (
|
||||
additionalLink: string,
|
||||
response: string,
|
||||
linkTitle: string,
|
||||
) => void;
|
||||
prefilledResponse?: string;
|
||||
}
|
||||
|
||||
|
@ -146,15 +143,20 @@ function ReferenceVerification(props: ReferenceVerificationProps) {
|
|||
setInitialResponse(props.prefilledResponse);
|
||||
setIsLoading(false);
|
||||
} else {
|
||||
verifyStatement(verificationStatement, props.conversationId, setIsLoading, setInitialResponse, () => { });
|
||||
verifyStatement(
|
||||
verificationStatement,
|
||||
props.conversationId,
|
||||
setIsLoading,
|
||||
setInitialResponse,
|
||||
() => {},
|
||||
);
|
||||
}
|
||||
|
||||
setIsMobileWidth(window.innerWidth < 768);
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
window.addEventListener("resize", () => {
|
||||
setIsMobileWidth(window.innerWidth < 768);
|
||||
})
|
||||
|
||||
});
|
||||
}, [verificationStatement, props.conversationId, props.prefilledResponse]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -163,27 +165,30 @@ function ReferenceVerification(props: ReferenceVerificationProps) {
|
|||
|
||||
if (!isLoading) {
|
||||
// Only set the child references when it's done loading and if the initial response is not prefilled (i.e. it was fetched from the server)
|
||||
props.setChildReferencesCallback(props.additionalLink, initialResponse, props.linkTitle);
|
||||
props.setChildReferencesCallback(
|
||||
props.additionalLink,
|
||||
initialResponse,
|
||||
props.linkTitle,
|
||||
);
|
||||
}
|
||||
}, [initialResponse, isLoading, props]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isLoading &&
|
||||
<LoadingSpinner />
|
||||
}
|
||||
<ChatMessage chatMessage={
|
||||
{
|
||||
{isLoading && <LoadingSpinner />}
|
||||
<ChatMessage
|
||||
chatMessage={{
|
||||
automationId: "",
|
||||
by: "AI",
|
||||
message: initialResponse,
|
||||
context: [],
|
||||
created: (new Date()).toISOString(),
|
||||
created: new Date().toISOString(),
|
||||
onlineContext: {},
|
||||
}}
|
||||
isMobileWidth={isMobileWidth} />
|
||||
isMobileWidth={isMobileWidth}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
interface SupplementalReferenceProps {
|
||||
|
@ -191,7 +196,11 @@ interface SupplementalReferenceProps {
|
|||
officialFactToVerify: string;
|
||||
conversationId: string;
|
||||
additionalLink: string;
|
||||
setChildReferencesCallback: (additionalLink: string, response: string, linkTitle: string) => void;
|
||||
setChildReferencesCallback: (
|
||||
additionalLink: string,
|
||||
response: string,
|
||||
linkTitle: string,
|
||||
) => void;
|
||||
prefilledResponse?: string;
|
||||
linkTitle?: string;
|
||||
}
|
||||
|
@ -202,7 +211,12 @@ function SupplementalReference(props: SupplementalReferenceProps) {
|
|||
return (
|
||||
<Card className={`mt-2 mb-4`}>
|
||||
<CardHeader>
|
||||
<a className={styles.titleLink} href={props.additionalLink} target="_blank" rel="noreferrer">
|
||||
<a
|
||||
className={styles.titleLink}
|
||||
href={props.additionalLink}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{linkTitle}
|
||||
</a>
|
||||
<WebPageLink {...linkAsWebpage} />
|
||||
|
@ -214,7 +228,8 @@ function SupplementalReference(props: SupplementalReferenceProps) {
|
|||
linkTitle={linkTitle}
|
||||
conversationId={props.conversationId}
|
||||
setChildReferencesCallback={props.setChildReferencesCallback}
|
||||
prefilledResponse={props.prefilledResponse} />
|
||||
prefilledResponse={props.prefilledResponse}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
@ -224,13 +239,17 @@ const WebPageLink = (webpage: WebPage) => {
|
|||
const webpageDomain = new URL(webpage.link).hostname;
|
||||
return (
|
||||
<div className={styles.subLinks}>
|
||||
<a className={`${styles.subLinks} bg-blue-200 px-2`} href={webpage.link} target="_blank" rel="noreferrer">
|
||||
<a
|
||||
className={`${styles.subLinks} bg-blue-200 px-2`}
|
||||
href={webpage.link}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{webpageDomain}
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
export default function FactChecker() {
|
||||
const [factToVerify, setFactToVerify] = useState("");
|
||||
|
@ -249,9 +268,15 @@ export default function FactChecker() {
|
|||
|
||||
const [initialModel, setInitialModel] = useState<Model>();
|
||||
|
||||
function setChildReferencesCallback(additionalLink: string, response: string, linkTitle: string) {
|
||||
function setChildReferencesCallback(
|
||||
additionalLink: string,
|
||||
response: string,
|
||||
linkTitle: string,
|
||||
) {
|
||||
const newReferences = childReferences || [];
|
||||
const exists = newReferences.find((reference) => reference.additionalLink === additionalLink);
|
||||
const exists = newReferences.find(
|
||||
(reference) => reference.additionalLink === additionalLink,
|
||||
);
|
||||
if (exists) return;
|
||||
newReferences.push({ additionalLink, response, linkTitle });
|
||||
setChildReferences(newReferences);
|
||||
|
@ -260,10 +285,9 @@ export default function FactChecker() {
|
|||
useEffect(() => {
|
||||
setIsMobileWidth(window.innerWidth < 768);
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
window.addEventListener("resize", () => {
|
||||
setIsMobileWidth(window.innerWidth < 768);
|
||||
})
|
||||
|
||||
});
|
||||
}, []);
|
||||
|
||||
let userData = useAuthenticatedData();
|
||||
|
@ -279,13 +303,13 @@ export default function FactChecker() {
|
|||
};
|
||||
|
||||
fetch(`/api/chat/store/factchecker`, {
|
||||
method: 'POST',
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"runId": runId,
|
||||
"storeData": data
|
||||
runId: runId,
|
||||
storeData: data,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
@ -294,25 +318,25 @@ export default function FactChecker() {
|
|||
if (factToVerify) {
|
||||
document.title = `AI Fact Check: ${factToVerify}`;
|
||||
} else {
|
||||
document.title = 'AI Fact Checker';
|
||||
document.title = "AI Fact Checker";
|
||||
}
|
||||
}, [factToVerify]);
|
||||
|
||||
useEffect(() => {
|
||||
const storedFact = localStorage.getItem('factToVerify');
|
||||
const storedFact = localStorage.getItem("factToVerify");
|
||||
if (storedFact) {
|
||||
setFactToVerify(storedFact);
|
||||
}
|
||||
|
||||
// Get query params from the URL
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const factToVerifyParam = urlParams.get('factToVerify');
|
||||
const factToVerifyParam = urlParams.get("factToVerify");
|
||||
|
||||
if (factToVerifyParam) {
|
||||
setFactToVerify(factToVerifyParam);
|
||||
}
|
||||
|
||||
const runIdParam = urlParams.get('runId');
|
||||
const runIdParam = urlParams.get("runId");
|
||||
if (runIdParam) {
|
||||
setRunId(runIdParam);
|
||||
|
||||
|
@ -341,7 +365,6 @@ export default function FactChecker() {
|
|||
// Call the async function
|
||||
fetchData();
|
||||
}
|
||||
|
||||
}, []);
|
||||
|
||||
function onClickVerify() {
|
||||
|
@ -365,9 +388,13 @@ export default function FactChecker() {
|
|||
spawnNewConversation(setConversationID);
|
||||
|
||||
// Set the runId to a random 12-digit alphanumeric string
|
||||
const newRunId = [...Array(16)].map(() => Math.random().toString(36)[2]).join('');
|
||||
const newRunId = [...Array(16)].map(() => Math.random().toString(36)[2]).join("");
|
||||
setRunId(newRunId);
|
||||
window.history.pushState({}, document.title, window.location.pathname + `?runId=${newRunId}`);
|
||||
window.history.pushState(
|
||||
{},
|
||||
document.title,
|
||||
window.location.pathname + `?runId=${newRunId}`,
|
||||
);
|
||||
|
||||
setOfficialFactToVerify(factToVerify);
|
||||
setClickedVerify(false);
|
||||
|
@ -375,37 +402,48 @@ export default function FactChecker() {
|
|||
|
||||
useEffect(() => {
|
||||
if (!conversationID) return;
|
||||
verifyStatement(officialFactToVerify, conversationID, setIsLoading, setInitialResponse, setInitialReferences);
|
||||
verifyStatement(
|
||||
officialFactToVerify,
|
||||
conversationID,
|
||||
setIsLoading,
|
||||
setInitialResponse,
|
||||
setInitialReferences,
|
||||
);
|
||||
}, [conversationID, officialFactToVerify]);
|
||||
|
||||
// Store factToVerify in localStorage whenever it changes
|
||||
useEffect(() => {
|
||||
localStorage.setItem('factToVerify', factToVerify);
|
||||
localStorage.setItem("factToVerify", factToVerify);
|
||||
}, [factToVerify]);
|
||||
|
||||
// Update the meta tags for the description and og:description
|
||||
useEffect(() => {
|
||||
let metaTag = document.querySelector('meta[name="description"]');
|
||||
if (metaTag) {
|
||||
metaTag.setAttribute('content', initialResponse);
|
||||
metaTag.setAttribute("content", initialResponse);
|
||||
}
|
||||
let metaOgTag = document.querySelector('meta[property="og:description"]');
|
||||
if (!metaOgTag) {
|
||||
metaOgTag = document.createElement('meta');
|
||||
metaOgTag.setAttribute('property', 'og:description');
|
||||
document.getElementsByTagName('head')[0].appendChild(metaOgTag);
|
||||
metaOgTag = document.createElement("meta");
|
||||
metaOgTag.setAttribute("property", "og:description");
|
||||
document.getElementsByTagName("head")[0].appendChild(metaOgTag);
|
||||
}
|
||||
metaOgTag.setAttribute('content', initialResponse);
|
||||
metaOgTag.setAttribute("content", initialResponse);
|
||||
}, [initialResponse]);
|
||||
|
||||
const renderReferences = (conversationId: string, initialReferences: ResponseWithReferences, officialFactToVerify: string, loadedFromStorage: boolean, childReferences?: SupplementReferences[]) => {
|
||||
const renderReferences = (
|
||||
conversationId: string,
|
||||
initialReferences: ResponseWithReferences,
|
||||
officialFactToVerify: string,
|
||||
loadedFromStorage: boolean,
|
||||
childReferences?: SupplementReferences[],
|
||||
) => {
|
||||
if (loadedFromStorage && childReferences) {
|
||||
return renderSupplementalReferences(childReferences);
|
||||
}
|
||||
|
||||
const seenLinks = new Set();
|
||||
|
||||
|
||||
// Any links that are present in webpages should not be searched again
|
||||
Object.entries(initialReferences.online || {}).map(([key, onlineData], index) => {
|
||||
const webpages = onlineData?.webpages || [];
|
||||
|
@ -413,7 +451,7 @@ export default function FactChecker() {
|
|||
if (webpages instanceof Array) {
|
||||
for (let i = 0; i < webpages.length; i++) {
|
||||
const webpage = webpages[i];
|
||||
const additionalLink = webpage.link || '';
|
||||
const additionalLink = webpage.link || "";
|
||||
if (seenLinks.has(additionalLink)) {
|
||||
return null;
|
||||
}
|
||||
|
@ -421,7 +459,7 @@ export default function FactChecker() {
|
|||
}
|
||||
} else {
|
||||
let singleWebpage = webpages as WebPage;
|
||||
const additionalLink = singleWebpage.link || '';
|
||||
const additionalLink = singleWebpage.link || "";
|
||||
if (seenLinks.has(additionalLink)) {
|
||||
return null;
|
||||
}
|
||||
|
@ -429,33 +467,36 @@ export default function FactChecker() {
|
|||
}
|
||||
});
|
||||
|
||||
return Object.entries(initialReferences.online || {}).map(([key, onlineData], index) => {
|
||||
let additionalLink = '';
|
||||
return Object.entries(initialReferences.online || {})
|
||||
.map(([key, onlineData], index) => {
|
||||
let additionalLink = "";
|
||||
|
||||
// Loop through organic links until we find one that hasn't been searched
|
||||
for (let i = 0; i < onlineData?.organic?.length; i++) {
|
||||
const webpage = onlineData?.organic?.[i];
|
||||
additionalLink = webpage.link || '';
|
||||
// Loop through organic links until we find one that hasn't been searched
|
||||
for (let i = 0; i < onlineData?.organic?.length; i++) {
|
||||
const webpage = onlineData?.organic?.[i];
|
||||
additionalLink = webpage.link || "";
|
||||
|
||||
if (!seenLinks.has(additionalLink)) {
|
||||
break;
|
||||
if (!seenLinks.has(additionalLink)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
seenLinks.add(additionalLink);
|
||||
seenLinks.add(additionalLink);
|
||||
|
||||
if (additionalLink === '') return null;
|
||||
if (additionalLink === "") return null;
|
||||
|
||||
return (
|
||||
<SupplementalReference
|
||||
key={index}
|
||||
onlineData={onlineData}
|
||||
officialFactToVerify={officialFactToVerify}
|
||||
conversationId={conversationId}
|
||||
additionalLink={additionalLink}
|
||||
setChildReferencesCallback={setChildReferencesCallback} />
|
||||
);
|
||||
}).filter(Boolean);
|
||||
return (
|
||||
<SupplementalReference
|
||||
key={index}
|
||||
onlineData={onlineData}
|
||||
officialFactToVerify={officialFactToVerify}
|
||||
conversationId={conversationId}
|
||||
additionalLink={additionalLink}
|
||||
setChildReferencesCallback={setChildReferencesCallback}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.filter(Boolean);
|
||||
};
|
||||
|
||||
const renderSupplementalReferences = (references: SupplementReferences[]) => {
|
||||
|
@ -468,10 +509,11 @@ export default function FactChecker() {
|
|||
conversationId={conversationID}
|
||||
linkTitle={reference.linkTitle}
|
||||
setChildReferencesCallback={setChildReferencesCallback}
|
||||
prefilledResponse={reference.response} />
|
||||
)
|
||||
prefilledResponse={reference.response}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const renderWebpages = (webpages: WebPage[] | WebPage) => {
|
||||
if (webpages instanceof Array) {
|
||||
|
@ -485,116 +527,138 @@ export default function FactChecker() {
|
|||
|
||||
function constructShareUrl() {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('runId', runId);
|
||||
url.searchParams.set("runId", runId);
|
||||
return url.href;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='relative md:fixed h-full'>
|
||||
<SidePanel
|
||||
conversationId={null}
|
||||
uploadedFiles={[]}
|
||||
isMobileWidth={isMobileWidth}
|
||||
/>
|
||||
<div className="relative md:fixed h-full">
|
||||
<SidePanel conversationId={null} uploadedFiles={[]} isMobileWidth={isMobileWidth} />
|
||||
</div>
|
||||
<div className={styles.factCheckerContainer}>
|
||||
<h1 className={`${styles.response} pt-8 md:pt-4 font-large outline-slate-800 dark:outline-slate-200`}>
|
||||
<h1
|
||||
className={`${styles.response} pt-8 md:pt-4 font-large outline-slate-800 dark:outline-slate-200`}
|
||||
>
|
||||
AI Fact Checker
|
||||
</h1>
|
||||
<footer className={`${styles.footer} mt-4`}>
|
||||
This is an experimental AI tool. It may make mistakes.
|
||||
</footer>
|
||||
{
|
||||
initialResponse && initialReferences && childReferences
|
||||
?
|
||||
<div className={styles.reportActions}>
|
||||
<Button asChild variant='secondary'>
|
||||
<Link href="/factchecker" target="_blank" rel="noopener noreferrer">
|
||||
Try Another
|
||||
</Link>
|
||||
{initialResponse && initialReferences && childReferences ? (
|
||||
<div className={styles.reportActions}>
|
||||
<Button asChild variant="secondary">
|
||||
<Link href="/factchecker" target="_blank" rel="noopener noreferrer">
|
||||
Try Another
|
||||
</Link>
|
||||
</Button>
|
||||
<ShareLink
|
||||
buttonTitle="Share report"
|
||||
title="AI Fact Checking Report"
|
||||
description="Share this fact checking report with others. Anyone who has this link will be able to view the report."
|
||||
url={constructShareUrl()}
|
||||
onShare={loadedFromStorage ? () => {} : storeData}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.newReportActions}>
|
||||
<div className={`${styles.inputFields} mt-4`}>
|
||||
<Input
|
||||
type="text"
|
||||
maxLength={200}
|
||||
placeholder="Enter a falsifiable statement to verify"
|
||||
disabled={isLoading}
|
||||
onChange={(e) => setFactToVerify(e.target.value)}
|
||||
value={factToVerify}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
onClickVerify();
|
||||
}
|
||||
}}
|
||||
onFocus={(e) => (e.target.placeholder = "")}
|
||||
onBlur={(e) =>
|
||||
(e.target.placeholder =
|
||||
"Enter a falsifiable statement to verify")
|
||||
}
|
||||
/>
|
||||
<Button disabled={clickedVerify} onClick={() => onClickVerify()}>
|
||||
Verify
|
||||
</Button>
|
||||
<ShareLink
|
||||
buttonTitle='Share report'
|
||||
title="AI Fact Checking Report"
|
||||
description="Share this fact checking report with others. Anyone who has this link will be able to view the report."
|
||||
url={constructShareUrl()}
|
||||
onShare={loadedFromStorage ? () => { } : storeData} />
|
||||
</div>
|
||||
: <div className={styles.newReportActions}>
|
||||
<div className={`${styles.inputFields} mt-4`}>
|
||||
<Input
|
||||
type="text"
|
||||
maxLength={200}
|
||||
placeholder="Enter a falsifiable statement to verify"
|
||||
disabled={isLoading}
|
||||
onChange={(e) => setFactToVerify(e.target.value)}
|
||||
value={factToVerify}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
onClickVerify();
|
||||
}
|
||||
}}
|
||||
onFocus={(e) => e.target.placeholder = ""}
|
||||
onBlur={(e) => e.target.placeholder = "Enter a falsifiable statement to verify"} />
|
||||
<Button disabled={clickedVerify} onClick={() => onClickVerify()}>Verify</Button>
|
||||
</div>
|
||||
<h3 className={`mt-4 mb-4`}>
|
||||
Try with a particular model. You must be <a href="/settings" className="font-medium text-blue-600 dark:text-blue-500 hover:underline">subscribed</a> to configure the model.
|
||||
</h3>
|
||||
</div>
|
||||
}
|
||||
<ModelPicker disabled={isLoading || loadedFromStorage} setModelUsed={setModelUsed} initialModel={initialModel} />
|
||||
{isLoading && <div className={styles.loading}>
|
||||
<LoadingSpinner />
|
||||
</div>}
|
||||
{
|
||||
initialResponse &&
|
||||
<h3 className={`mt-4 mb-4`}>
|
||||
Try with a particular model. You must be{" "}
|
||||
<a
|
||||
href="/settings"
|
||||
className="font-medium text-blue-600 dark:text-blue-500 hover:underline"
|
||||
>
|
||||
subscribed
|
||||
</a>{" "}
|
||||
to configure the model.
|
||||
</h3>
|
||||
</div>
|
||||
)}
|
||||
<ModelPicker
|
||||
disabled={isLoading || loadedFromStorage}
|
||||
setModelUsed={setModelUsed}
|
||||
initialModel={initialModel}
|
||||
/>
|
||||
{isLoading && (
|
||||
<div className={styles.loading}>
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
)}
|
||||
{initialResponse && (
|
||||
<Card className={`mt-4`}>
|
||||
<CardHeader>
|
||||
<CardTitle>{officialFactToVerify}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className={styles.responseText}>
|
||||
<ChatMessage chatMessage={
|
||||
{
|
||||
<ChatMessage
|
||||
chatMessage={{
|
||||
automationId: "",
|
||||
by: "AI",
|
||||
message: initialResponse,
|
||||
context: [],
|
||||
created: (new Date()).toISOString(),
|
||||
onlineContext: {}
|
||||
}
|
||||
} isMobileWidth={isMobileWidth} />
|
||||
|
||||
created: new Date().toISOString(),
|
||||
onlineContext: {},
|
||||
}}
|
||||
isMobileWidth={isMobileWidth}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
{
|
||||
initialReferences && initialReferences.online && Object.keys(initialReferences.online).length > 0 && (
|
||||
{initialReferences &&
|
||||
initialReferences.online &&
|
||||
Object.keys(initialReferences.online).length > 0 && (
|
||||
<div className={styles.subLinks}>
|
||||
{
|
||||
Object.entries(initialReferences.online).map(([key, onlineData], index) => {
|
||||
{Object.entries(initialReferences.online).map(
|
||||
([key, onlineData], index) => {
|
||||
const webpages = onlineData?.webpages || [];
|
||||
return renderWebpages(webpages);
|
||||
})
|
||||
}
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
}
|
||||
{
|
||||
initialReferences &&
|
||||
)}
|
||||
{initialReferences && (
|
||||
<div className={styles.referenceContainer}>
|
||||
<h2 className="mt-4 mb-4">Supplements</h2>
|
||||
<div className={styles.references}>
|
||||
{initialReferences.online !== undefined &&
|
||||
renderReferences(conversationID, initialReferences, officialFactToVerify, loadedFromStorage, childReferences)}
|
||||
renderReferences(
|
||||
conversationID,
|
||||
initialReferences,
|
||||
officialFactToVerify,
|
||||
loadedFromStorage,
|
||||
childReferences,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -8,20 +8,21 @@ export const metadata: Metadata = {
|
|||
title: "Khoj AI - Home",
|
||||
description: "Your Second Brain.",
|
||||
icons: {
|
||||
icon: '/static/favicon.ico',
|
||||
icon: "/static/favicon.ico",
|
||||
},
|
||||
manifest: '/static/khoj.webmanifest',
|
||||
manifest: "/static/khoj.webmanifest",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<meta httpEquiv="Content-Security-Policy"
|
||||
content="default-src 'self' https://assets.khoj.dev;
|
||||
return (
|
||||
<html lang="en">
|
||||
<meta
|
||||
httpEquiv="Content-Security-Policy"
|
||||
content="default-src 'self' https://assets.khoj.dev;
|
||||
media-src * blob:;
|
||||
script-src 'self' https://assets.khoj.dev 'unsafe-inline' 'unsafe-eval';
|
||||
connect-src 'self' https://ipapi.co/json ws://localhost:42110;
|
||||
|
@ -29,10 +30,9 @@ export default function RootLayout({
|
|||
img-src 'self' data: https://*.khoj.dev https://*.googleusercontent.com https://*.google.com/ https://*.gstatic.com;
|
||||
font-src 'self' https://assets.khoj.dev https://fonts.gstatic.com;
|
||||
child-src 'none';
|
||||
object-src 'none';"></meta>
|
||||
<body className={inter.className}>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
object-src 'none';"
|
||||
></meta>
|
||||
<body className={inter.className}>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,27 +1,26 @@
|
|||
'use client'
|
||||
import './globals.css';
|
||||
import styles from './page.module.css';
|
||||
import 'katex/dist/katex.min.css';
|
||||
"use client";
|
||||
import "./globals.css";
|
||||
import styles from "./page.module.css";
|
||||
import "katex/dist/katex.min.css";
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import Image from 'next/image';
|
||||
import { ArrowCounterClockwise, ClockCounterClockwise } from '@phosphor-icons/react';
|
||||
import React, { useEffect, useState } from "react";
|
||||
import useSWR from "swr";
|
||||
import Image from "next/image";
|
||||
import { ArrowCounterClockwise, ClockCounterClockwise } from "@phosphor-icons/react";
|
||||
|
||||
import { Card, CardTitle } from '@/components/ui/card';
|
||||
import SuggestionCard from '@/app/components/suggestions/suggestionCard';
|
||||
import SidePanel from '@/app/components/sidePanel/chatHistorySidePanel';
|
||||
import Loading from '@/app/components/loading/loading';
|
||||
import ChatInputArea, { ChatOptions } from '@/app/components/chatInputArea/chatInputArea';
|
||||
import { Suggestion, suggestionsData } from '@/app/components/suggestions/suggestionsData';
|
||||
import LoginPrompt from '@/app/components/loginPrompt/loginPrompt';
|
||||
|
||||
import { useAuthenticatedData, UserConfig, useUserConfig } from '@/app/common/auth';
|
||||
import { convertColorToBorderClass } from '@/app/common/colorUtils';
|
||||
import { getIconFromIconName } from '@/app/common/iconUtils';
|
||||
import { AgentData } from '@/app/agents/page';
|
||||
import { createNewConversation } from './common/chatFunctions';
|
||||
import { Card, CardTitle } from "@/components/ui/card";
|
||||
import SuggestionCard from "@/app/components/suggestions/suggestionCard";
|
||||
import SidePanel from "@/app/components/sidePanel/chatHistorySidePanel";
|
||||
import Loading from "@/app/components/loading/loading";
|
||||
import ChatInputArea, { ChatOptions } from "@/app/components/chatInputArea/chatInputArea";
|
||||
import { Suggestion, suggestionsData } from "@/app/components/suggestions/suggestionsData";
|
||||
import LoginPrompt from "@/app/components/loginPrompt/loginPrompt";
|
||||
|
||||
import { useAuthenticatedData, UserConfig, useUserConfig } from "@/app/common/auth";
|
||||
import { convertColorToBorderClass } from "@/app/common/colorUtils";
|
||||
import { getIconFromIconName } from "@/app/common/iconUtils";
|
||||
import { AgentData } from "@/app/agents/page";
|
||||
import { createNewConversation } from "./common/chatFunctions";
|
||||
|
||||
interface ChatBodyDataProps {
|
||||
chatOptionsData: ChatOptions | null;
|
||||
|
@ -34,9 +33,9 @@ interface ChatBodyDataProps {
|
|||
}
|
||||
|
||||
function ChatBodyData(props: ChatBodyDataProps) {
|
||||
const [message, setMessage] = useState('');
|
||||
const [message, setMessage] = useState("");
|
||||
const [processingMessage, setProcessingMessage] = useState(false);
|
||||
const [greeting, setGreeting] = useState('');
|
||||
const [greeting, setGreeting] = useState("");
|
||||
const [shuffledOptions, setShuffledOptions] = useState<Suggestion[]>([]);
|
||||
const [selectedAgent, setSelectedAgent] = useState<string | null>("khoj");
|
||||
const [agentIcons, setAgentIcons] = useState<JSX.Element[]>([]);
|
||||
|
@ -45,8 +44,14 @@ function ChatBodyData(props: ChatBodyDataProps) {
|
|||
|
||||
const onConversationIdChange = props.onConversationIdChange;
|
||||
|
||||
const agentsFetcher = () => window.fetch('/api/agents').then(res => res.json()).catch(err => console.log(err));
|
||||
const { data: agentsData, error } = useSWR<AgentData[]>('agents', agentsFetcher, { revalidateOnFocus: false });
|
||||
const agentsFetcher = () =>
|
||||
window
|
||||
.fetch("/api/agents")
|
||||
.then((res) => res.json())
|
||||
.catch((err) => console.log(err));
|
||||
const { data: agentsData, error } = useSWR<AgentData[]>("agents", agentsFetcher, {
|
||||
revalidateOnFocus: false,
|
||||
});
|
||||
|
||||
function shuffleAndSetOptions() {
|
||||
const shuffled = [...suggestionsData].sort(() => 0.5 - Math.random());
|
||||
|
@ -59,14 +64,19 @@ function ChatBodyData(props: ChatBodyDataProps) {
|
|||
// Get today's day
|
||||
const today = new Date();
|
||||
const day = today.getDay();
|
||||
const timeOfDay = today.getHours() >= 17 || today.getHours() < 4 ? 'evening' : today.getHours() >= 12 ? 'afternoon' : 'morning';
|
||||
const timeOfDay =
|
||||
today.getHours() >= 17 || today.getHours() < 4
|
||||
? "evening"
|
||||
: today.getHours() >= 12
|
||||
? "afternoon"
|
||||
: "morning";
|
||||
const nameSuffix = props.userConfig?.given_name ? `, ${props.userConfig?.given_name}` : "";
|
||||
const greetings = [
|
||||
`What would you like to get done${nameSuffix}?`,
|
||||
`Hey${nameSuffix}! How can I help?`,
|
||||
`Good ${timeOfDay}${nameSuffix}! What's on your mind?`,
|
||||
`Ready to breeze through your ${['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][day]}?`,
|
||||
`Want help navigating your ${['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][day]} workload?`
|
||||
`Ready to breeze through your ${["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][day]}?`,
|
||||
`Want help navigating your ${["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][day]} workload?`,
|
||||
];
|
||||
const greeting = greetings[Math.floor(Math.random() * greetings.length)];
|
||||
setGreeting(greeting);
|
||||
|
@ -83,8 +93,8 @@ function ChatBodyData(props: ChatBodyDataProps) {
|
|||
const shuffledAgents = agentsData ? [...agentsData].sort(() => 0.5 - Math.random()) : [];
|
||||
const agents = agentsData ? [agentsData[0]] : []; // Always add the first/default agent.
|
||||
|
||||
shuffledAgents.slice(0, nSlice - 1).forEach(agent => {
|
||||
if (!agents.find(a => a.slug === agent.slug)) {
|
||||
shuffledAgents.slice(0, nSlice - 1).forEach((agent) => {
|
||||
if (!agents.find((a) => a.slug === agent.slug)) {
|
||||
agents.push(agent);
|
||||
}
|
||||
});
|
||||
|
@ -93,7 +103,16 @@ function ChatBodyData(props: ChatBodyDataProps) {
|
|||
|
||||
//generate colored icons for the selected agents
|
||||
const agentIcons = agents.map(
|
||||
agent => getIconFromIconName(agent.icon, agent.color) || <Image key={agent.name} src={agent.avatar} alt={agent.name} width={50} height={50} />
|
||||
(agent) =>
|
||||
getIconFromIconName(agent.icon, agent.color) || (
|
||||
<Image
|
||||
key={agent.name}
|
||||
src={agent.avatar}
|
||||
alt={agent.name}
|
||||
width={50}
|
||||
height={50}
|
||||
/>
|
||||
),
|
||||
);
|
||||
setAgentIcons(agentIcons);
|
||||
}, [agentsData, props.isMobileWidth]);
|
||||
|
@ -110,19 +129,18 @@ function ChatBodyData(props: ChatBodyDataProps) {
|
|||
const newConversationId = await createNewConversation(selectedAgent || "khoj");
|
||||
onConversationIdChange?.(newConversationId);
|
||||
window.location.href = `/chat?conversationId=${newConversationId}`;
|
||||
localStorage.setItem('message', message);
|
||||
}
|
||||
catch (error) {
|
||||
localStorage.setItem("message", message);
|
||||
} catch (error) {
|
||||
console.error("Error creating new conversation:", error);
|
||||
setProcessingMessage(false);
|
||||
}
|
||||
setMessage('');
|
||||
setMessage("");
|
||||
}
|
||||
};
|
||||
processMessage();
|
||||
if (message) {
|
||||
setProcessingMessage(true);
|
||||
};
|
||||
}
|
||||
}, [selectedAgent, message, processingMessage, onConversationIdChange]);
|
||||
|
||||
function fillArea(link: string, type: string, prompt: string) {
|
||||
|
@ -150,44 +168,54 @@ function ChatBodyData(props: ChatBodyDataProps) {
|
|||
|
||||
return (
|
||||
<div className={`${styles.homeGreetings} w-full md:w-auto`}>
|
||||
{
|
||||
showLoginPrompt && (
|
||||
<LoginPrompt
|
||||
onOpenChange={setShowLoginPrompt}
|
||||
loginRedirectMessage={"Login to your second brain"} />
|
||||
)
|
||||
}
|
||||
{showLoginPrompt && (
|
||||
<LoginPrompt
|
||||
onOpenChange={setShowLoginPrompt}
|
||||
loginRedirectMessage={"Login to your second brain"}
|
||||
/>
|
||||
)}
|
||||
<div className={`w-full text-center justify-end content-end`}>
|
||||
<div className="items-center">
|
||||
<h1 className="text-2xl md:text-3xl text-center w-fit pb-6 px-4 mx-auto">{greeting}</h1>
|
||||
<h1 className="text-2xl md:text-3xl text-center w-fit pb-6 px-4 mx-auto">
|
||||
{greeting}
|
||||
</h1>
|
||||
</div>
|
||||
{
|
||||
!props.isMobileWidth &&
|
||||
{!props.isMobileWidth && (
|
||||
<div className="flex pb-6 gap-2 items-center justify-center">
|
||||
{agentIcons.map((icon, index) => (
|
||||
<Card
|
||||
key={`${index}-${agents[index].slug}`}
|
||||
className={
|
||||
`${selectedAgent === agents[index].slug ?
|
||||
convertColorToBorderClass(agents[index].color) : 'border-stone-100 dark:border-neutral-700 text-muted-foreground'}
|
||||
hover:cursor-pointer rounded-lg px-2 py-2`}>
|
||||
className={`${
|
||||
selectedAgent === agents[index].slug
|
||||
? convertColorToBorderClass(agents[index].color)
|
||||
: "border-stone-100 dark:border-neutral-700 text-muted-foreground"
|
||||
}
|
||||
hover:cursor-pointer rounded-lg px-2 py-2`}
|
||||
>
|
||||
<CardTitle
|
||||
className='text-center text-md font-medium flex justify-center items-center'
|
||||
onClick={() => setSelectedAgent(agents[index].slug)}>
|
||||
className="text-center text-md font-medium flex justify-center items-center"
|
||||
onClick={() => setSelectedAgent(agents[index].slug)}
|
||||
>
|
||||
{icon} {agents[index].name}
|
||||
</CardTitle>
|
||||
</Card>
|
||||
))}
|
||||
<Card className='border-none shadow-none flex justify-center items-center hover:cursor-pointer' onClick={() => window.location.href = "/agents"}>
|
||||
<CardTitle className="text-center text-md font-normal flex justify-center items-center px-1.5 py-2">See All →</CardTitle>
|
||||
<Card
|
||||
className="border-none shadow-none flex justify-center items-center hover:cursor-pointer"
|
||||
onClick={() => (window.location.href = "/agents")}
|
||||
>
|
||||
<CardTitle className="text-center text-md font-normal flex justify-center items-center px-1.5 py-2">
|
||||
See All →
|
||||
</CardTitle>
|
||||
</Card>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
<div className={`mx-auto ${props.isMobileWidth ? 'w-full' : 'w-fit'}`}>
|
||||
{
|
||||
!props.isMobileWidth &&
|
||||
<div className={`w-full ${styles.inputBox} shadow-lg bg-background align-middle items-center justify-center px-3 py-1 dark:bg-neutral-700 border-stone-100 dark:border-none dark:shadow-none rounded-2xl`}>
|
||||
<div className={`mx-auto ${props.isMobileWidth ? "w-full" : "w-fit"}`}>
|
||||
{!props.isMobileWidth && (
|
||||
<div
|
||||
className={`w-full ${styles.inputBox} shadow-lg bg-background align-middle items-center justify-center px-3 py-1 dark:bg-neutral-700 border-stone-100 dark:border-none dark:shadow-none rounded-2xl`}
|
||||
>
|
||||
<ChatInputArea
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
sendMessage={(message) => setMessage(message)}
|
||||
|
@ -195,22 +223,30 @@ function ChatBodyData(props: ChatBodyDataProps) {
|
|||
chatOptionsData={props.chatOptionsData}
|
||||
conversationId={null}
|
||||
isMobileWidth={props.isMobileWidth}
|
||||
setUploadedFiles={props.setUploadedFiles} />
|
||||
setUploadedFiles={props.setUploadedFiles}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
<div className={`${styles.suggestions} w-full ${props.isMobileWidth ? 'grid' : 'flex flex-row'} justify-center items-center`}>
|
||||
)}
|
||||
<div
|
||||
className={`${styles.suggestions} w-full ${props.isMobileWidth ? "grid" : "flex flex-row"} justify-center items-center`}
|
||||
>
|
||||
{shuffledOptions.map((suggestion, index) => (
|
||||
<div
|
||||
key={`${suggestion.type} ${suggestion.description}`}
|
||||
onClick={(event) => {
|
||||
if (props.isLoggedIn) {
|
||||
fillArea(suggestion.link, suggestion.type, suggestion.description);
|
||||
fillArea(
|
||||
suggestion.link,
|
||||
suggestion.type,
|
||||
suggestion.description,
|
||||
);
|
||||
} else {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setShowLoginPrompt(true);
|
||||
}
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
<SuggestionCard
|
||||
key={suggestion.type + Math.random()}
|
||||
title={suggestion.type}
|
||||
|
@ -224,31 +260,40 @@ function ChatBodyData(props: ChatBodyDataProps) {
|
|||
<div className="flex items-center justify-center margin-auto">
|
||||
<button
|
||||
onClick={shuffleSuggestionsCards}
|
||||
className="m-2 p-1.5 rounded-lg dark:hover:bg-[var(--background-color)] hover:bg-stone-100 border border-stone-100 text-sm text-stone-500 dark:text-stone-300 dark:border-neutral-700">
|
||||
More Ideas <ArrowCounterClockwise className='h-4 w-4 inline' />
|
||||
className="m-2 p-1.5 rounded-lg dark:hover:bg-[var(--background-color)] hover:bg-stone-100 border border-stone-100 text-sm text-stone-500 dark:text-stone-300 dark:border-neutral-700"
|
||||
>
|
||||
More Ideas <ArrowCounterClockwise className="h-4 w-4 inline" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
props.isMobileWidth &&
|
||||
{props.isMobileWidth && (
|
||||
<>
|
||||
<div className={`${styles.inputBox} pt-1 shadow-[0_-20px_25px_-5px_rgba(0,0,0,0.1)] dark:bg-neutral-700 bg-background align-middle items-center justify-center pb-3 mx-1 rounded-t-2xl rounded-b-none`}>
|
||||
<div
|
||||
className={`${styles.inputBox} pt-1 shadow-[0_-20px_25px_-5px_rgba(0,0,0,0.1)] dark:bg-neutral-700 bg-background align-middle items-center justify-center pb-3 mx-1 rounded-t-2xl rounded-b-none`}
|
||||
>
|
||||
<div className="flex gap-2 items-center justify-left pt-1 pb-2 px-12">
|
||||
{agentIcons.map((icon, index) => (
|
||||
<Card
|
||||
key={`${index}-${agents[index].slug}`}
|
||||
className={
|
||||
`${selectedAgent === agents[index].slug ? convertColorToBorderClass(agents[index].color) : 'border-muted text-muted-foreground'} hover:cursor-pointer`
|
||||
}>
|
||||
className={`${selectedAgent === agents[index].slug ? convertColorToBorderClass(agents[index].color) : "border-muted text-muted-foreground"} hover:cursor-pointer`}
|
||||
>
|
||||
<CardTitle
|
||||
className='text-center text-xs font-medium flex justify-center items-center px-1.5 py-1'
|
||||
onClick={() => setSelectedAgent(agents[index].slug)}>
|
||||
className="text-center text-xs font-medium flex justify-center items-center px-1.5 py-1"
|
||||
onClick={() => setSelectedAgent(agents[index].slug)}
|
||||
>
|
||||
{icon} {agents[index].name}
|
||||
</CardTitle>
|
||||
</Card>
|
||||
))}
|
||||
<Card className='border-none shadow-none flex justify-center items-center hover:cursor-pointer' onClick={() => window.location.href = "/agents"}>
|
||||
<CardTitle className={`text-center ${props.isMobileWidth ? 'text-xs' : 'text-md'} font-normal flex justify-center items-center px-1.5 py-2`}>See All →</CardTitle>
|
||||
<Card
|
||||
className="border-none shadow-none flex justify-center items-center hover:cursor-pointer"
|
||||
onClick={() => (window.location.href = "/agents")}
|
||||
>
|
||||
<CardTitle
|
||||
className={`text-center ${props.isMobileWidth ? "text-xs" : "text-md"} font-normal flex justify-center items-center px-1.5 py-2`}
|
||||
>
|
||||
See All →
|
||||
</CardTitle>
|
||||
</Card>
|
||||
</div>
|
||||
<ChatInputArea
|
||||
|
@ -258,10 +303,11 @@ function ChatBodyData(props: ChatBodyDataProps) {
|
|||
chatOptionsData={props.chatOptionsData}
|
||||
conversationId={null}
|
||||
isMobileWidth={props.isMobileWidth}
|
||||
setUploadedFiles={props.setUploadedFiles} />
|
||||
setUploadedFiles={props.setUploadedFiles}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -273,7 +319,7 @@ export default function Home() {
|
|||
const [uploadedFiles, setUploadedFiles] = useState<string[]>([]);
|
||||
const [isMobileWidth, setIsMobileWidth] = useState(false);
|
||||
|
||||
const {userConfig: initialUserConfig, isLoadingUserConfig} = useUserConfig(true);
|
||||
const { userConfig: initialUserConfig, isLoadingUserConfig } = useUserConfig(true);
|
||||
const [userConfig, setUserConfig] = useState<UserConfig | null>(null);
|
||||
|
||||
const authenticatedData = useAuthenticatedData();
|
||||
|
@ -287,25 +333,24 @@ export default function Home() {
|
|||
}, [initialUserConfig]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/chat/options')
|
||||
.then(response => response.json())
|
||||
fetch("/api/chat/options")
|
||||
.then((response) => response.json())
|
||||
.then((data: ChatOptions) => {
|
||||
setLoading(false);
|
||||
if (data) {
|
||||
setChatOptionsData(data);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
return;
|
||||
});
|
||||
|
||||
setIsMobileWidth(window.innerWidth < 786);
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
window.addEventListener("resize", () => {
|
||||
setIsMobileWidth(window.innerWidth < 786);
|
||||
});
|
||||
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
|
@ -314,9 +359,7 @@ export default function Home() {
|
|||
|
||||
return (
|
||||
<div className={`${styles.main} ${styles.chatLayout}`}>
|
||||
<title>
|
||||
Khoj AI - Your Second Brain
|
||||
</title>
|
||||
<title>Khoj AI - Your Second Brain</title>
|
||||
<div className={`${styles.sidePanel}`}>
|
||||
<SidePanel
|
||||
conversationId={conversationId}
|
||||
|
|
|
@ -2,12 +2,12 @@ import type { Metadata } from "next";
|
|||
|
||||
import "../globals.css";
|
||||
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Khoj AI - Search",
|
||||
description: "Find anything in documents you've shared with Khoj using natural language queries.",
|
||||
description:
|
||||
"Find anything in documents you've shared with Khoj using natural language queries.",
|
||||
icons: {
|
||||
icon: '/static/favicon.ico',
|
||||
icon: "/static/favicon.ico",
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -16,9 +16,5 @@ export default function RootLayout({
|
|||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
return <div>{children}</div>;
|
||||
}
|
||||
|
|
|
@ -1,16 +1,36 @@
|
|||
'use client'
|
||||
"use client";
|
||||
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
import { useAuthenticatedData } from '../common/auth';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import SidePanel from '../components/sidePanel/chatHistorySidePanel';
|
||||
import styles from './search.module.css';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { ArrowLeft, ArrowRight, FileDashed, FileMagnifyingGlass, Folder, FolderOpen, GithubLogo, Lightbulb, LinkSimple, MagnifyingGlass, NoteBlank, NotionLogo } from '@phosphor-icons/react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import Link from 'next/link';
|
||||
import { useAuthenticatedData } from "../common/auth";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import SidePanel from "../components/sidePanel/chatHistorySidePanel";
|
||||
import styles from "./search.module.css";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
FileDashed,
|
||||
FileMagnifyingGlass,
|
||||
Folder,
|
||||
FolderOpen,
|
||||
GithubLogo,
|
||||
Lightbulb,
|
||||
LinkSimple,
|
||||
MagnifyingGlass,
|
||||
NoteBlank,
|
||||
NotionLogo,
|
||||
} from "@phosphor-icons/react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Link from "next/link";
|
||||
|
||||
interface AdditionalData {
|
||||
file: string;
|
||||
|
@ -28,13 +48,13 @@ interface SearchResult {
|
|||
}
|
||||
|
||||
function getNoteTypeIcon(source: string) {
|
||||
if (source === 'notion') {
|
||||
return <NotionLogo className='text-muted-foreground' />;
|
||||
if (source === "notion") {
|
||||
return <NotionLogo className="text-muted-foreground" />;
|
||||
}
|
||||
if (source === 'github') {
|
||||
return <GithubLogo className='text-muted-foreground' />;
|
||||
if (source === "github") {
|
||||
return <GithubLogo className="text-muted-foreground" />;
|
||||
}
|
||||
return <NoteBlank className='text-muted-foreground' />;
|
||||
return <NoteBlank className="text-muted-foreground" />;
|
||||
}
|
||||
|
||||
const naturalLanguageSearchQueryExamples = [
|
||||
|
@ -54,7 +74,7 @@ const naturalLanguageSearchQueryExamples = [
|
|||
"Fish farming Kenya 2021",
|
||||
"How to make a cake without an oven",
|
||||
"Installing a solar panel at home",
|
||||
]
|
||||
];
|
||||
|
||||
interface NoteResultProps {
|
||||
note: SearchResult;
|
||||
|
@ -63,69 +83,80 @@ interface NoteResultProps {
|
|||
|
||||
function Note(props: NoteResultProps) {
|
||||
const note = props.note;
|
||||
const isFileNameURL = (note.additional.file || '').startsWith('http');
|
||||
const fileName = isFileNameURL ? note.additional.heading : note.additional.file.split('/').pop();
|
||||
const isFileNameURL = (note.additional.file || "").startsWith("http");
|
||||
const fileName = isFileNameURL
|
||||
? note.additional.heading
|
||||
: note.additional.file.split("/").pop();
|
||||
|
||||
return (
|
||||
<Card className='bg-secondary h-full shadow-sm rounded-lg bg-gradient-to-b from-background to-slate-50 dark:to-gray-950 border border-muted mb-4'>
|
||||
<Card className="bg-secondary h-full shadow-sm rounded-lg bg-gradient-to-b from-background to-slate-50 dark:to-gray-950 border border-muted mb-4">
|
||||
<CardHeader>
|
||||
<CardDescription className='p-1 border-muted border w-fit rounded-lg mb-2'>
|
||||
<CardDescription className="p-1 border-muted border w-fit rounded-lg mb-2">
|
||||
{getNoteTypeIcon(note.additional.source)}
|
||||
</CardDescription>
|
||||
<CardTitle>
|
||||
{fileName}
|
||||
</CardTitle>
|
||||
<CardTitle>{fileName}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className='line-clamp-4 text-muted-foreground'>
|
||||
{note.entry}
|
||||
</div>
|
||||
<Button onClick={() => props.setFocusSearchResult(note)} variant={'ghost'} className='p-0 mt-2 text-orange-400 hover:bg-inherit'>
|
||||
See content<ArrowRight className='inline ml-2' />
|
||||
<div className="line-clamp-4 text-muted-foreground">{note.entry}</div>
|
||||
<Button
|
||||
onClick={() => props.setFocusSearchResult(note)}
|
||||
variant={"ghost"}
|
||||
className="p-0 mt-2 text-orange-400 hover:bg-inherit"
|
||||
>
|
||||
See content
|
||||
<ArrowRight className="inline ml-2" />
|
||||
</Button>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
{
|
||||
isFileNameURL ?
|
||||
<a href={note.additional.file} target="_blank" className='underline text-sm bg-muted p-1 rounded-lg text-muted-foreground'>
|
||||
<LinkSimple className='inline m-2' />{note.additional.file}
|
||||
</a>
|
||||
:
|
||||
<div className='bg-muted p-1 text-sm rounded-lg text-muted-foreground'>
|
||||
<FolderOpen className='inline m-2' />{note.additional.file}
|
||||
</div>
|
||||
}
|
||||
{isFileNameURL ? (
|
||||
<a
|
||||
href={note.additional.file}
|
||||
target="_blank"
|
||||
className="underline text-sm bg-muted p-1 rounded-lg text-muted-foreground"
|
||||
>
|
||||
<LinkSimple className="inline m-2" />
|
||||
{note.additional.file}
|
||||
</a>
|
||||
) : (
|
||||
<div className="bg-muted p-1 text-sm rounded-lg text-muted-foreground">
|
||||
<FolderOpen className="inline m-2" />
|
||||
{note.additional.file}
|
||||
</div>
|
||||
)}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function focusNote(note: SearchResult) {
|
||||
const isFileNameURL = (note.additional.file || '').startsWith('http');
|
||||
const fileName = isFileNameURL ? note.additional.heading : note.additional.file.split('/').pop();
|
||||
const isFileNameURL = (note.additional.file || "").startsWith("http");
|
||||
const fileName = isFileNameURL
|
||||
? note.additional.heading
|
||||
: note.additional.file.split("/").pop();
|
||||
return (
|
||||
<Card className='bg-secondary h-full shadow-sm rounded-lg bg-gradient-to-b from-background to-slate-50 dark:to-gray-950 border border-muted mb-4'>
|
||||
<Card className="bg-secondary h-full shadow-sm rounded-lg bg-gradient-to-b from-background to-slate-50 dark:to-gray-950 border border-muted mb-4">
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
{fileName}
|
||||
</CardTitle>
|
||||
<CardTitle>{fileName}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardFooter>
|
||||
{
|
||||
isFileNameURL ?
|
||||
<a href={note.additional.file} target="_blank" className='underline text-sm bg-muted p-3 rounded-lg text-muted-foreground flex items-center gap-2'>
|
||||
<LinkSimple className='inline' />{note.additional.file}
|
||||
</a>
|
||||
:
|
||||
<div className='bg-muted p-3 text-sm rounded-lg text-muted-foreground flex items-center gap-2'>
|
||||
<FolderOpen className='inline' />{note.additional.file}
|
||||
</div>
|
||||
}
|
||||
{isFileNameURL ? (
|
||||
<a
|
||||
href={note.additional.file}
|
||||
target="_blank"
|
||||
className="underline text-sm bg-muted p-3 rounded-lg text-muted-foreground flex items-center gap-2"
|
||||
>
|
||||
<LinkSimple className="inline" />
|
||||
{note.additional.file}
|
||||
</a>
|
||||
) : (
|
||||
<div className="bg-muted p-3 text-sm rounded-lg text-muted-foreground flex items-center gap-2">
|
||||
<FolderOpen className="inline" />
|
||||
{note.additional.file}
|
||||
</div>
|
||||
)}
|
||||
</CardFooter>
|
||||
<CardContent>
|
||||
<div className='text-m'>
|
||||
{note.entry}
|
||||
</div>
|
||||
<div className="text-m">{note.entry}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
@ -133,29 +164,31 @@ function focusNote(note: SearchResult) {
|
|||
|
||||
export default function Search() {
|
||||
const authenticatedData = useAuthenticatedData();
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [isMobileWidth, setIsMobileWidth] = useState(false);
|
||||
const [title, setTitle] = useState('Search');
|
||||
const [title, setTitle] = useState("Search");
|
||||
const [searchResults, setSearchResults] = useState<SearchResult[] | null>(null);
|
||||
const [searchResultsLoading, setSearchResultsLoading] = useState(false);
|
||||
const [focusSearchResult, setFocusSearchResult] = useState<SearchResult | null>(null);
|
||||
const [exampleQuery, setExampleQuery] = useState('');
|
||||
const [exampleQuery, setExampleQuery] = useState("");
|
||||
const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setIsMobileWidth(window.innerWidth < 786);
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
window.addEventListener("resize", () => {
|
||||
setIsMobileWidth(window.innerWidth < 786);
|
||||
});
|
||||
|
||||
setExampleQuery(naturalLanguageSearchQueryExamples[Math.floor(Math.random() * naturalLanguageSearchQueryExamples.length)]);
|
||||
|
||||
setExampleQuery(
|
||||
naturalLanguageSearchQueryExamples[
|
||||
Math.floor(Math.random() * naturalLanguageSearchQueryExamples.length)
|
||||
],
|
||||
);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setTitle(isMobileWidth ? '' : 'Search');
|
||||
setTitle(isMobileWidth ? "" : "Search");
|
||||
}, [isMobileWidth]);
|
||||
|
||||
function search() {
|
||||
|
@ -163,16 +196,18 @@ export default function Search() {
|
|||
|
||||
const apiUrl = `/api/search?q=${encodeURIComponent(searchQuery)}&client=web`;
|
||||
fetch(apiUrl, {
|
||||
method: 'GET',
|
||||
method: "GET",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
}).then(response => response.json())
|
||||
.then(data => {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
setSearchResults(data);
|
||||
setSearchResultsLoading(false);
|
||||
}).catch((error) => {
|
||||
console.error('Error:', error);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error:", error);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -198,99 +233,104 @@ export default function Search() {
|
|||
clearTimeout(searchTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
|
||||
}, [searchQuery]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={`h-full ${styles.sidePanel}`}>
|
||||
<SidePanel
|
||||
conversationId={null}
|
||||
uploadedFiles={[]}
|
||||
isMobileWidth={isMobileWidth}
|
||||
/>
|
||||
<SidePanel conversationId={null} uploadedFiles={[]} isMobileWidth={isMobileWidth} />
|
||||
</div>
|
||||
<div className={`${styles.searchLayout}`}>
|
||||
<div className="md:w-3/4 sm:w-full mx-auto pt-6 md:pt-8">
|
||||
<div className='p-4 md:w-3/4 sm:w-full mx-auto'>
|
||||
<div className='flex justify-between items-center border-2 border-muted p-2 gap-4 rounded-lg'>
|
||||
<MagnifyingGlass className='inline m-2 h-4 w-4' />
|
||||
<div className="p-4 md:w-3/4 sm:w-full mx-auto">
|
||||
<div className="flex justify-between items-center border-2 border-muted p-2 gap-4 rounded-lg">
|
||||
<MagnifyingGlass className="inline m-2 h-4 w-4" />
|
||||
<Input
|
||||
autoFocus={true}
|
||||
className='border-none'
|
||||
className="border-none"
|
||||
onChange={(e) => setSearchQuery(e.currentTarget.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && search()}
|
||||
onKeyDown={(e) => e.key === "Enter" && search()}
|
||||
type="search"
|
||||
placeholder="Search Documents" />
|
||||
<button className='px-4 rounded' onClick={() => search()}>
|
||||
placeholder="Search Documents"
|
||||
/>
|
||||
<button className="px-4 rounded" onClick={() => search()}>
|
||||
Find
|
||||
</button>
|
||||
</div>
|
||||
{
|
||||
focusSearchResult &&
|
||||
<div className='mt-4'>
|
||||
<Button onClick={() => setFocusSearchResult(null)} className='mb-4' variant={'outline'}>
|
||||
<ArrowLeft className='inline mr-2' />
|
||||
{focusSearchResult && (
|
||||
<div className="mt-4">
|
||||
<Button
|
||||
onClick={() => setFocusSearchResult(null)}
|
||||
className="mb-4"
|
||||
variant={"outline"}
|
||||
>
|
||||
<ArrowLeft className="inline mr-2" />
|
||||
Back
|
||||
</Button>
|
||||
{focusNote(focusSearchResult)}
|
||||
</div>
|
||||
}
|
||||
{
|
||||
!focusSearchResult && searchResults && searchResults.length > 0 &&
|
||||
<div className='mt-4 max-w-[92vw] break-all'>
|
||||
)}
|
||||
{!focusSearchResult && searchResults && searchResults.length > 0 && (
|
||||
<div className="mt-4 max-w-[92vw] break-all">
|
||||
<ScrollArea className="h-[80vh]">
|
||||
{
|
||||
searchResults.map((result, index) => {
|
||||
return (
|
||||
<Note key={result["corpus-id"]}
|
||||
note={result}
|
||||
setFocusSearchResult={setFocusSearchResult} />
|
||||
);
|
||||
})
|
||||
}
|
||||
{searchResults.map((result, index) => {
|
||||
return (
|
||||
<Note
|
||||
key={result["corpus-id"]}
|
||||
note={result}
|
||||
setFocusSearchResult={setFocusSearchResult}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ScrollArea>
|
||||
</div>
|
||||
}
|
||||
{
|
||||
searchResults == null &&
|
||||
<Card className='flex flex-col items-center justify-center border-none shadow-none'>
|
||||
<CardHeader className='flex flex-col items-center justify-center'>
|
||||
<CardDescription className='border-muted-foreground border w-fit rounded-lg mb-2 text-center text-lg p-4'>
|
||||
<FileMagnifyingGlass weight='fill' className='text-muted-foreground h-10 w-10' />
|
||||
)}
|
||||
{searchResults == null && (
|
||||
<Card className="flex flex-col items-center justify-center border-none shadow-none">
|
||||
<CardHeader className="flex flex-col items-center justify-center">
|
||||
<CardDescription className="border-muted-foreground border w-fit rounded-lg mb-2 text-center text-lg p-4">
|
||||
<FileMagnifyingGlass
|
||||
weight="fill"
|
||||
className="text-muted-foreground h-10 w-10"
|
||||
/>
|
||||
</CardDescription>
|
||||
<CardTitle className='text-center'>
|
||||
<CardTitle className="text-center">
|
||||
Search across your documents
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className='text-muted-foreground items-center justify-center text-center flex'>
|
||||
<Lightbulb className='inline mr-2' /> {exampleQuery}
|
||||
<CardContent className="text-muted-foreground items-center justify-center text-center flex">
|
||||
<Lightbulb className="inline mr-2" /> {exampleQuery}
|
||||
</CardContent>
|
||||
</Card>
|
||||
}
|
||||
{
|
||||
searchResults && searchResults.length === 0 &&
|
||||
<Card className='flex flex-col items-center justify-center border-none shadow-none'>
|
||||
<CardHeader className='flex flex-col items-center justify-center'>
|
||||
<CardDescription className='border-muted-foreground border w-fit rounded-lg mb-2 text-center text-lg p-4'>
|
||||
<FileDashed weight='fill' className='text-muted-foreground h-10 w-10' />
|
||||
)}
|
||||
{searchResults && searchResults.length === 0 && (
|
||||
<Card className="flex flex-col items-center justify-center border-none shadow-none">
|
||||
<CardHeader className="flex flex-col items-center justify-center">
|
||||
<CardDescription className="border-muted-foreground border w-fit rounded-lg mb-2 text-center text-lg p-4">
|
||||
<FileDashed
|
||||
weight="fill"
|
||||
className="text-muted-foreground h-10 w-10"
|
||||
/>
|
||||
</CardDescription>
|
||||
<CardTitle className='text-center'>
|
||||
<CardTitle className="text-center">
|
||||
No documents found
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className='text-muted-foreground items-center justify-center text-center flex'>
|
||||
<div className="text-muted-foreground items-center justify-center text-center flex">
|
||||
To use search, upload your docs to your account.
|
||||
</div>
|
||||
<Link href="https://docs.khoj.dev/data-sources/share_your_data" className='no-underline'>
|
||||
<div className='mt-4 text-center text-secondary-foreground bg-secondary w-fit m-auto p-2 rounded-lg'>
|
||||
<Link
|
||||
href="https://docs.khoj.dev/data-sources/share_your_data"
|
||||
className="no-underline"
|
||||
>
|
||||
<div className="mt-4 text-center text-secondary-foreground bg-secondary w-fit m-auto p-2 rounded-lg">
|
||||
Learn More
|
||||
</div>
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -20,7 +20,8 @@ export default function RootLayout({
|
|||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<meta httpEquiv="Content-Security-Policy"
|
||||
<meta
|
||||
httpEquiv="Content-Security-Policy"
|
||||
content="default-src 'self' https://assets.khoj.dev;
|
||||
script-src 'self' https://assets.khoj.dev 'unsafe-inline' 'unsafe-eval';
|
||||
connect-src 'self' https://ipapi.co/json ws://localhost:42110;
|
||||
|
@ -28,7 +29,8 @@ export default function RootLayout({
|
|||
img-src 'self' data: https://*.khoj.dev https://*.googleusercontent.com;
|
||||
font-src 'self' https://assets.khoj.dev https://fonts.gstatic.com;
|
||||
child-src 'none';
|
||||
object-src 'none';"></meta>
|
||||
object-src 'none';"
|
||||
></meta>
|
||||
<body className={inter.className}>
|
||||
{children}
|
||||
<Toaster />
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -5,29 +5,29 @@ import "../../globals.css";
|
|||
const inter = Noto_Sans({ subsets: ["latin"] });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Khoj AI - Chat",
|
||||
description: "Use this page to view a chat with Khoj AI.",
|
||||
title: "Khoj AI - Chat",
|
||||
description: "Use this page to view a chat with Khoj AI.",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<meta httpEquiv="Content-Security-Policy"
|
||||
content="default-src 'self' https://assets.khoj.dev;
|
||||
return (
|
||||
<html lang="en">
|
||||
<meta
|
||||
httpEquiv="Content-Security-Policy"
|
||||
content="default-src 'self' https://assets.khoj.dev;
|
||||
script-src 'self' https://assets.khoj.dev 'unsafe-inline' 'unsafe-eval';
|
||||
connect-src 'self' https://ipapi.co/json ws://localhost:42110;
|
||||
style-src 'self' https://assets.khoj.dev 'unsafe-inline' https://fonts.googleapis.com;
|
||||
img-src 'self' data: https://*.khoj.dev https://*.googleusercontent.com https://*.google.com/ https://*.gstatic.com;
|
||||
font-src 'self' https://assets.khoj.dev https://fonts.gstatic.com;
|
||||
child-src 'none';
|
||||
object-src 'none';"></meta>
|
||||
<body className={inter.className}>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
object-src 'none';"
|
||||
></meta>
|
||||
<body className={inter.className}>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,23 +1,22 @@
|
|||
'use client'
|
||||
"use client";
|
||||
|
||||
import styles from './sharedChat.module.css';
|
||||
import React, { Suspense, useEffect, useRef, useState } from 'react';
|
||||
import styles from "./sharedChat.module.css";
|
||||
import React, { Suspense, useEffect, useRef, useState } from "react";
|
||||
|
||||
import SidePanel from '../../components/sidePanel/chatHistorySidePanel';
|
||||
import ChatHistory from '../../components/chatHistory/chatHistory';
|
||||
import NavMenu from '../../components/navMenu/navMenu';
|
||||
import Loading from '../../components/loading/loading';
|
||||
import SidePanel from "../../components/sidePanel/chatHistorySidePanel";
|
||||
import ChatHistory from "../../components/chatHistory/chatHistory";
|
||||
import NavMenu from "../../components/navMenu/navMenu";
|
||||
import Loading from "../../components/loading/loading";
|
||||
|
||||
import 'katex/dist/katex.min.css';
|
||||
import "katex/dist/katex.min.css";
|
||||
|
||||
import { useIPLocationData, welcomeConsole } from '../../common/utils';
|
||||
import { useAuthenticatedData } from '@/app/common/auth';
|
||||
|
||||
import ChatInputArea, { ChatOptions } from '@/app/components/chatInputArea/chatInputArea';
|
||||
import { StreamMessage } from '@/app/components/chatMessage/chatMessage';
|
||||
import { processMessageChunk } from '@/app/common/chatFunctions';
|
||||
import { AgentData } from '@/app/agents/page';
|
||||
import { useIPLocationData, welcomeConsole } from "../../common/utils";
|
||||
import { useAuthenticatedData } from "@/app/common/auth";
|
||||
|
||||
import ChatInputArea, { ChatOptions } from "@/app/components/chatInputArea/chatInputArea";
|
||||
import { StreamMessage } from "@/app/components/chatMessage/chatMessage";
|
||||
import { processMessageChunk } from "@/app/common/chatFunctions";
|
||||
import { AgentData } from "@/app/agents/page";
|
||||
|
||||
interface ChatBodyDataProps {
|
||||
chatOptionsData: ChatOptions | null;
|
||||
|
@ -31,13 +30,12 @@ interface ChatBodyDataProps {
|
|||
setQueryToProcess: (query: string) => void;
|
||||
}
|
||||
|
||||
|
||||
function ChatBodyData(props: ChatBodyDataProps) {
|
||||
const [message, setMessage] = useState('');
|
||||
const [message, setMessage] = useState("");
|
||||
const [processingMessage, setProcessingMessage] = useState(false);
|
||||
const [agentMetadata, setAgentMetadata] = useState<AgentData | null>(null);
|
||||
|
||||
const setQueryToProcess = props.setQueryToProcess
|
||||
const setQueryToProcess = props.setQueryToProcess;
|
||||
const streamedMessages = props.streamedMessages;
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -48,22 +46,19 @@ function ChatBodyData(props: ChatBodyDataProps) {
|
|||
}, [message, setQueryToProcess]);
|
||||
|
||||
useEffect(() => {
|
||||
if (streamedMessages &&
|
||||
if (
|
||||
streamedMessages &&
|
||||
streamedMessages.length > 0 &&
|
||||
streamedMessages[streamedMessages.length - 1].completed) {
|
||||
|
||||
streamedMessages[streamedMessages.length - 1].completed
|
||||
) {
|
||||
setProcessingMessage(false);
|
||||
} else {
|
||||
setMessage('');
|
||||
setMessage("");
|
||||
}
|
||||
}, [streamedMessages]);
|
||||
|
||||
if (!props.publicConversationSlug && !props.conversationId) {
|
||||
return (
|
||||
<div className={styles.suggestions}>
|
||||
Whoops, nothing to see here!
|
||||
</div>
|
||||
);
|
||||
return <div className={styles.suggestions}>Whoops, nothing to see here!</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -71,13 +66,16 @@ function ChatBodyData(props: ChatBodyDataProps) {
|
|||
<div className={false ? styles.chatBody : styles.chatBodyFull}>
|
||||
<ChatHistory
|
||||
publicConversationSlug={props.publicConversationSlug}
|
||||
conversationId={props.conversationId || ''}
|
||||
conversationId={props.conversationId || ""}
|
||||
setAgent={setAgentMetadata}
|
||||
setTitle={props.setTitle}
|
||||
pendingMessage={processingMessage ? message : ''}
|
||||
incomingMessages={props.streamedMessages} />
|
||||
pendingMessage={processingMessage ? message : ""}
|
||||
incomingMessages={props.streamedMessages}
|
||||
/>
|
||||
</div>
|
||||
<div className={`${styles.inputBox} shadow-md bg-background align-middle items-center justify-center px-3`}>
|
||||
<div
|
||||
className={`${styles.inputBox} shadow-md bg-background align-middle items-center justify-center px-3`}
|
||||
>
|
||||
<ChatInputArea
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
sendMessage={(message) => setMessage(message)}
|
||||
|
@ -86,7 +84,8 @@ function ChatBodyData(props: ChatBodyDataProps) {
|
|||
conversationId={props.conversationId}
|
||||
agentColor={agentMetadata?.color}
|
||||
isMobileWidth={props.isMobileWidth}
|
||||
setUploadedFiles={props.setUploadedFiles} />
|
||||
setUploadedFiles={props.setUploadedFiles}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
@ -95,10 +94,10 @@ function ChatBodyData(props: ChatBodyDataProps) {
|
|||
export default function SharedChat() {
|
||||
const [chatOptionsData, setChatOptionsData] = useState<ChatOptions | null>(null);
|
||||
const [isLoading, setLoading] = useState(true);
|
||||
const [title, setTitle] = useState('Khoj AI - Chat');
|
||||
const [title, setTitle] = useState("Khoj AI - Chat");
|
||||
const [conversationId, setConversationID] = useState<string | undefined>(undefined);
|
||||
const [messages, setMessages] = useState<StreamMessage[]>([]);
|
||||
const [queryToProcess, setQueryToProcess] = useState<string>('');
|
||||
const [queryToProcess, setQueryToProcess] = useState<string>("");
|
||||
const [processQuerySignal, setProcessQuerySignal] = useState(false);
|
||||
const [uploadedFiles, setUploadedFiles] = useState<string[]>([]);
|
||||
const [isMobileWidth, setIsMobileWidth] = useState(false);
|
||||
|
@ -108,8 +107,8 @@ export default function SharedChat() {
|
|||
const authenticatedData = useAuthenticatedData();
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/chat/options')
|
||||
.then(response => response.json())
|
||||
fetch("/api/chat/options")
|
||||
.then((response) => response.json())
|
||||
.then((data: ChatOptions) => {
|
||||
setLoading(false);
|
||||
// Render chat options, if any
|
||||
|
@ -117,7 +116,7 @@ export default function SharedChat() {
|
|||
setChatOptionsData(data);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
return;
|
||||
});
|
||||
|
@ -126,35 +125,33 @@ export default function SharedChat() {
|
|||
|
||||
setIsMobileWidth(window.innerWidth < 786);
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
window.addEventListener("resize", () => {
|
||||
setIsMobileWidth(window.innerWidth < 786);
|
||||
});
|
||||
|
||||
setParamSlug(window.location.pathname.split('/').pop() || '');
|
||||
|
||||
setParamSlug(window.location.pathname.split("/").pop() || "");
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (queryToProcess && !conversationId) {
|
||||
// If the user has not yet started conversing in the chat, create a new conversation
|
||||
fetch(`/api/chat/share/fork?public_conversation_slug=${paramSlug}`, {
|
||||
method: 'POST',
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
setConversationID(data.conversation_id);
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
return;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (queryToProcess) {
|
||||
// Add a new object to the state
|
||||
const newStreamMessage: StreamMessage = {
|
||||
|
@ -163,10 +160,10 @@ export default function SharedChat() {
|
|||
context: [],
|
||||
onlineContext: {},
|
||||
completed: false,
|
||||
timestamp: (new Date()).toISOString(),
|
||||
timestamp: new Date().toISOString(),
|
||||
rawQuery: queryToProcess || "",
|
||||
}
|
||||
setMessages(prevMessages => [...prevMessages, newStreamMessage]);
|
||||
};
|
||||
setMessages((prevMessages) => [...prevMessages, newStreamMessage]);
|
||||
setProcessQuerySignal(true);
|
||||
}
|
||||
}, [queryToProcess, conversationId, paramSlug]);
|
||||
|
@ -177,21 +174,19 @@ export default function SharedChat() {
|
|||
}
|
||||
}, [processQuerySignal]);
|
||||
|
||||
|
||||
async function readChatStream(response: Response) {
|
||||
if (!response.ok) throw new Error(response.statusText);
|
||||
if (!response.body) throw new Error("Response body is null");
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
const eventDelimiter = '␃🔚␗';
|
||||
const eventDelimiter = "␃🔚␗";
|
||||
let buffer = "";
|
||||
|
||||
while (true) {
|
||||
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
setQueryToProcess('');
|
||||
setQueryToProcess("");
|
||||
setProcessQuerySignal(false);
|
||||
break;
|
||||
}
|
||||
|
@ -205,7 +200,7 @@ export default function SharedChat() {
|
|||
const event = buffer.slice(0, newEventIndex);
|
||||
buffer = buffer.slice(newEventIndex + eventDelimiter.length);
|
||||
if (event) {
|
||||
const currentMessage = messages.find(message => !message.completed);
|
||||
const currentMessage = messages.find((message) => !message.completed);
|
||||
|
||||
if (!currentMessage) {
|
||||
console.error("No current message found");
|
||||
|
@ -217,7 +212,6 @@ export default function SharedChat() {
|
|||
setMessages([...messages]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -246,11 +240,11 @@ export default function SharedChat() {
|
|||
context: [],
|
||||
onlineContext: {},
|
||||
completed: false,
|
||||
timestamp: (new Date()).toISOString(),
|
||||
timestamp: new Date().toISOString(),
|
||||
rawQuery: queryToProcess || "",
|
||||
}
|
||||
};
|
||||
setProcessQuerySignal(true);
|
||||
setMessages(prevMessages => [...prevMessages, newStreamMessage]);
|
||||
setMessages((prevMessages) => [...prevMessages, newStreamMessage]);
|
||||
}
|
||||
})();
|
||||
}, [conversationId, queryToProcess]);
|
||||
|
@ -260,19 +254,12 @@ export default function SharedChat() {
|
|||
}
|
||||
|
||||
if (!paramSlug) {
|
||||
return (
|
||||
<div className={styles.suggestions}>
|
||||
Whoops, nothing to see here!
|
||||
</div>
|
||||
);
|
||||
return <div className={styles.suggestions}>Whoops, nothing to see here!</div>;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className={`${styles.main} ${styles.chatLayout}`}>
|
||||
<title>
|
||||
{title}
|
||||
</title>
|
||||
<title>{title}</title>
|
||||
<div className={styles.sidePanel}>
|
||||
<SidePanel
|
||||
conversationId={conversationId ?? null}
|
||||
|
@ -293,10 +280,11 @@ export default function SharedChat() {
|
|||
chatOptionsData={chatOptionsData}
|
||||
setTitle={setTitle}
|
||||
setUploadedFiles={setUploadedFiles}
|
||||
isMobileWidth={isMobileWidth} />
|
||||
isMobileWidth={isMobileWidth}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,141 +1,117 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
||||
import * as React from "react";
|
||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
import { cn } from "@/lib/utils";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
|
||||
const AlertDialog = AlertDialogPrimitive.Root
|
||||
const AlertDialog = AlertDialogPrimitive.Root;
|
||||
|
||||
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
|
||||
const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
|
||||
|
||||
const AlertDialogPortal = AlertDialogPrimitive.Portal
|
||||
const AlertDialogPortal = AlertDialogPrimitive.Portal;
|
||||
|
||||
const AlertDialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
))
|
||||
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
|
||||
<AlertDialogPrimitive.Overlay
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
));
|
||||
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
|
||||
|
||||
const AlertDialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.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 bg-background 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}
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.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 bg-background 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}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
));
|
||||
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
|
||||
|
||||
const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
|
||||
);
|
||||
AlertDialogHeader.displayName = "AlertDialogHeader";
|
||||
|
||||
const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
))
|
||||
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
|
||||
|
||||
const AlertDialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-2 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
AlertDialogHeader.displayName = "AlertDialogHeader"
|
||||
|
||||
const AlertDialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
AlertDialogFooter.displayName = "AlertDialogFooter"
|
||||
);
|
||||
AlertDialogFooter.displayName = "AlertDialogFooter";
|
||||
|
||||
const AlertDialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-lg font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
|
||||
<AlertDialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-lg font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
|
||||
|
||||
const AlertDialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogDescription.displayName =
|
||||
AlertDialogPrimitive.Description.displayName
|
||||
<AlertDialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName;
|
||||
|
||||
const AlertDialogAction = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Action
|
||||
ref={ref}
|
||||
className={cn(buttonVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
|
||||
<AlertDialogPrimitive.Action ref={ref} className={cn(buttonVariants(), className)} {...props} />
|
||||
));
|
||||
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
|
||||
|
||||
const AlertDialogCancel = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Cancel
|
||||
ref={ref}
|
||||
className={cn(
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"mt-2 sm:mt-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
||||
<AlertDialogPrimitive.Cancel
|
||||
ref={ref}
|
||||
className={cn(buttonVariants({ variant: "outline" }), "mt-2 sm:mt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
|
||||
|
||||
export {
|
||||
AlertDialog,
|
||||
AlertDialogPortal,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogTrigger,
|
||||
AlertDialogContent,
|
||||
AlertDialogHeader,
|
||||
AlertDialogFooter,
|
||||
AlertDialogTitle,
|
||||
AlertDialogDescription,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
}
|
||||
AlertDialog,
|
||||
AlertDialogPortal,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogTrigger,
|
||||
AlertDialogContent,
|
||||
AlertDialogHeader,
|
||||
AlertDialogFooter,
|
||||
AlertDialogTitle,
|
||||
AlertDialogDescription,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
};
|
||||
|
|
|
@ -1,59 +1,49 @@
|
|||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import * as React from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-background text-foreground",
|
||||
destructive:
|
||||
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
||||
},
|
||||
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-background text-foreground",
|
||||
destructive:
|
||||
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const Alert = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
||||
>(({ className, variant, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Alert.displayName = "Alert"
|
||||
<div ref={ref} role="alert" className={cn(alertVariants({ variant }), className)} {...props} />
|
||||
));
|
||||
Alert.displayName = "Alert";
|
||||
|
||||
const AlertTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h5
|
||||
ref={ref}
|
||||
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertTitle.displayName = "AlertTitle"
|
||||
const AlertTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<h5
|
||||
ref={ref}
|
||||
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
AlertTitle.displayName = "AlertTitle";
|
||||
|
||||
const AlertDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDescription.displayName = "AlertDescription"
|
||||
<div ref={ref} className={cn("text-sm [&_p]:leading-relaxed", className)} {...props} />
|
||||
));
|
||||
AlertDescription.displayName = "AlertDescription";
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription }
|
||||
export { Alert, AlertTitle, AlertDescription };
|
||||
|
|
|
@ -1,50 +1,47 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
||||
import * as React from "react";
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Avatar = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
||||
React.ElementRef<typeof AvatarPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Avatar.displayName = AvatarPrimitive.Root.displayName
|
||||
<AvatarPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Avatar.displayName = AvatarPrimitive.Root.displayName;
|
||||
|
||||
const AvatarImage = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Image>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
||||
React.ElementRef<typeof AvatarPrimitive.Image>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Image
|
||||
ref={ref}
|
||||
className={cn("aspect-square h-full w-full", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AvatarImage.displayName = AvatarPrimitive.Image.displayName
|
||||
<AvatarPrimitive.Image
|
||||
ref={ref}
|
||||
className={cn("aspect-square h-full w-full", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
|
||||
|
||||
const AvatarFallback = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
||||
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Fallback
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
|
||||
<AvatarPrimitive.Fallback
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback }
|
||||
export { Avatar, AvatarImage, AvatarFallback };
|
||||
|
|
|
@ -1,56 +1,54 @@
|
|||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
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"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
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"
|
||||
({ 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 }
|
||||
export { Button, buttonVariants };
|
||||
|
|
|
@ -1,79 +1,56 @@
|
|||
import * as React from "react"
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Card.displayName = "Card"
|
||||
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("rounded-lg border bg-card text-card-foreground shadow-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
Card.displayName = "Card";
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardHeader.displayName = "CardHeader"
|
||||
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
|
||||
),
|
||||
);
|
||||
CardHeader.displayName = "CardHeader";
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-2xl font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardTitle.displayName = "CardTitle"
|
||||
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn("text-2xl font-semibold leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
CardTitle.displayName = "CardTitle";
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardDescription.displayName = "CardDescription"
|
||||
<p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
|
||||
));
|
||||
CardDescription.displayName = "CardDescription";
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
))
|
||||
CardContent.displayName = "CardContent"
|
||||
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
),
|
||||
);
|
||||
CardContent.displayName = "CardContent";
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardFooter.displayName = "CardFooter"
|
||||
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
|
||||
),
|
||||
);
|
||||
CardFooter.displayName = "CardFooter";
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
|
||||
|
||||
const Collapsible = CollapsiblePrimitive.Root
|
||||
const Collapsible = CollapsiblePrimitive.Root;
|
||||
|
||||
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
|
||||
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
|
||||
|
||||
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
|
||||
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
|
||||
|
|
|
@ -1,155 +1,145 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import { type DialogProps } from "@radix-ui/react-dialog"
|
||||
import { Command as CommandPrimitive } from "cmdk"
|
||||
import { Search } from "lucide-react"
|
||||
import * as React from "react";
|
||||
import { type DialogProps } from "@radix-ui/react-dialog";
|
||||
import { Command as CommandPrimitive } from "cmdk";
|
||||
import { Search } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Dialog, DialogContent } from "@/components/ui/dialog"
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
||||
|
||||
const Command = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
||||
React.ElementRef<typeof CommandPrimitive>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Command.displayName = CommandPrimitive.displayName
|
||||
<CommandPrimitive
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Command.displayName = CommandPrimitive.displayName;
|
||||
|
||||
interface CommandDialogProps extends DialogProps {}
|
||||
|
||||
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogContent className="overflow-hidden p-0 shadow-lg">
|
||||
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogContent className="overflow-hidden p-0 shadow-lg">
|
||||
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const CommandInput = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Input>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
||||
React.ElementRef<typeof CommandPrimitive.Input>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
|
||||
<Search className={cn("mr-2 h-4 w-4 shrink-0 opacity-50", className)} />
|
||||
<CommandPrimitive.Input
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
|
||||
<Search className={cn("mr-2 h-4 w-4 shrink-0 opacity-50", className)} />
|
||||
<CommandPrimitive.Input
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
|
||||
CommandInput.displayName = CommandPrimitive.Input.displayName
|
||||
CommandInput.displayName = CommandPrimitive.Input.displayName;
|
||||
|
||||
const CommandList = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
||||
React.ElementRef<typeof CommandPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.List
|
||||
ref={ref}
|
||||
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
<CommandPrimitive.List
|
||||
ref={ref}
|
||||
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandList.displayName = CommandPrimitive.List.displayName
|
||||
CommandList.displayName = CommandPrimitive.List.displayName;
|
||||
|
||||
const CommandEmpty = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Empty>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
||||
React.ElementRef<typeof CommandPrimitive.Empty>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
||||
>((props, ref) => (
|
||||
<CommandPrimitive.Empty
|
||||
ref={ref}
|
||||
className="py-6 text-center text-sm"
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
<CommandPrimitive.Empty ref={ref} className="py-6 text-center text-sm" {...props} />
|
||||
));
|
||||
|
||||
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
|
||||
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
|
||||
|
||||
const CommandGroup = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Group>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
||||
React.ElementRef<typeof CommandPrimitive.Group>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Group
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
<CommandPrimitive.Group
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandGroup.displayName = CommandPrimitive.Group.displayName
|
||||
CommandGroup.displayName = CommandPrimitive.Group.displayName;
|
||||
|
||||
const CommandSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
||||
React.ElementRef<typeof CommandPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 h-px bg-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
|
||||
<CommandPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 h-px bg-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
|
||||
|
||||
const CommandItem = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
||||
React.ElementRef<typeof CommandPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
CommandItem.displayName = CommandPrimitive.Item.displayName
|
||||
|
||||
const CommandShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"ml-auto text-xs tracking-widest text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
<CommandPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
CommandShortcut.displayName = "CommandShortcut"
|
||||
));
|
||||
|
||||
CommandItem.displayName = CommandPrimitive.Item.displayName;
|
||||
|
||||
const CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
CommandShortcut.displayName = "CommandShortcut";
|
||||
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandSeparator,
|
||||
}
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandSeparator,
|
||||
};
|
||||
|
|
|
@ -1,122 +1,107 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { X } from "lucide-react"
|
||||
import * as React from "react";
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Dialog = DialogPrimitive.Root
|
||||
const Dialog = DialogPrimitive.Root;
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger
|
||||
const DialogTrigger = DialogPrimitive.Trigger;
|
||||
|
||||
const DialogPortal = DialogPrimitive.Portal
|
||||
const DialogPortal = DialogPrimitive.Portal;
|
||||
|
||||
const DialogClose = DialogPrimitive.Close
|
||||
const DialogClose = DialogPrimitive.Close;
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
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/80 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
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 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>
|
||||
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 bg-background 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-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">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
))
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||
<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 bg-background 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-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">
|
||||
<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 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 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>
|
||||
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
|
||||
<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>
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogClose,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
}
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogClose,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
};
|
||||
|
|
|
@ -1,118 +1,100 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import { Drawer as DrawerPrimitive } from "vaul"
|
||||
import * as React from "react";
|
||||
import { Drawer as DrawerPrimitive } from "vaul";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Drawer = ({
|
||||
shouldScaleBackground = true,
|
||||
...props
|
||||
shouldScaleBackground = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
|
||||
<DrawerPrimitive.Root
|
||||
shouldScaleBackground={shouldScaleBackground}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
Drawer.displayName = "Drawer"
|
||||
<DrawerPrimitive.Root shouldScaleBackground={shouldScaleBackground} {...props} />
|
||||
);
|
||||
Drawer.displayName = "Drawer";
|
||||
|
||||
const DrawerTrigger = DrawerPrimitive.Trigger
|
||||
const DrawerTrigger = DrawerPrimitive.Trigger;
|
||||
|
||||
const DrawerPortal = DrawerPrimitive.Portal
|
||||
const DrawerPortal = DrawerPrimitive.Portal;
|
||||
|
||||
const DrawerClose = DrawerPrimitive.Close
|
||||
const DrawerClose = DrawerPrimitive.Close;
|
||||
|
||||
const DrawerOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
|
||||
React.ElementRef<typeof DrawerPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DrawerPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn("fixed inset-0 z-50 bg-black/80", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
|
||||
<DrawerPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn("fixed inset-0 z-50 bg-black/80", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
|
||||
|
||||
const DrawerContent = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
|
||||
React.ElementRef<typeof DrawerPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DrawerPortal>
|
||||
<DrawerOverlay />
|
||||
<DrawerPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
|
||||
{children}
|
||||
</DrawerPrimitive.Content>
|
||||
</DrawerPortal>
|
||||
))
|
||||
DrawerContent.displayName = "DrawerContent"
|
||||
<DrawerPortal>
|
||||
<DrawerOverlay />
|
||||
<DrawerPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
|
||||
{children}
|
||||
</DrawerPrimitive.Content>
|
||||
</DrawerPortal>
|
||||
));
|
||||
DrawerContent.displayName = "DrawerContent";
|
||||
|
||||
const DrawerHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DrawerHeader.displayName = "DrawerHeader"
|
||||
const DrawerHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)} {...props} />
|
||||
);
|
||||
DrawerHeader.displayName = "DrawerHeader";
|
||||
|
||||
const DrawerFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DrawerFooter.displayName = "DrawerFooter"
|
||||
const DrawerFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn("mt-auto flex flex-col gap-2 p-4", className)} {...props} />
|
||||
);
|
||||
DrawerFooter.displayName = "DrawerFooter";
|
||||
|
||||
const DrawerTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
|
||||
React.ElementRef<typeof DrawerPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DrawerPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DrawerTitle.displayName = DrawerPrimitive.Title.displayName
|
||||
<DrawerPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
|
||||
|
||||
const DrawerDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
|
||||
React.ElementRef<typeof DrawerPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DrawerPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DrawerDescription.displayName = DrawerPrimitive.Description.displayName
|
||||
<DrawerPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Drawer,
|
||||
DrawerPortal,
|
||||
DrawerOverlay,
|
||||
DrawerTrigger,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerHeader,
|
||||
DrawerFooter,
|
||||
DrawerTitle,
|
||||
DrawerDescription,
|
||||
}
|
||||
Drawer,
|
||||
DrawerPortal,
|
||||
DrawerOverlay,
|
||||
DrawerTrigger,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerHeader,
|
||||
DrawerFooter,
|
||||
DrawerTitle,
|
||||
DrawerDescription,
|
||||
};
|
||||
|
|
|
@ -1,200 +1,187 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||
import { Check, ChevronRight, Circle } from "lucide-react"
|
||||
import * as React from "react";
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
import { Check, ChevronRight, Circle } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root;
|
||||
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
||||
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
||||
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
||||
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
||||
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
}
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto h-4 w-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
))
|
||||
DropdownMenuSubTrigger.displayName =
|
||||
DropdownMenuPrimitive.SubTrigger.displayName
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto h-4 w-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
));
|
||||
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSubContent.displayName =
|
||||
DropdownMenuPrimitive.SubContent.displayName
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
))
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
));
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
}
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
))
|
||||
DropdownMenuCheckboxItem.displayName =
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
));
|
||||
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Circle className="h-2 w-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
))
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Circle className="h-2 w-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
));
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
}
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
||||
|
||||
const DropdownMenuShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||
{...props}
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
||||
));
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
||||
|
||||
const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span className={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...props} />
|
||||
);
|
||||
};
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
}
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
};
|
||||
|
|
|
@ -1,178 +1,171 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
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"
|
||||
Controller,
|
||||
ControllerProps,
|
||||
FieldPath,
|
||||
FieldValues,
|
||||
FormProvider,
|
||||
useFormContext,
|
||||
} from "react-hook-form";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
const Form = FormProvider
|
||||
const Form = FormProvider;
|
||||
|
||||
type FormFieldContextValue<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
> = {
|
||||
name: TName
|
||||
}
|
||||
name: TName;
|
||||
};
|
||||
|
||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||
{} as FormFieldContextValue
|
||||
)
|
||||
const FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue);
|
||||
|
||||
const FormField = <
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
>({
|
||||
...props
|
||||
...props
|
||||
}: ControllerProps<TFieldValues, TName>) => {
|
||||
return (
|
||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||
<Controller {...props} />
|
||||
</FormFieldContext.Provider>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||
<Controller {...props} />
|
||||
</FormFieldContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const useFormField = () => {
|
||||
const fieldContext = React.useContext(FormFieldContext)
|
||||
const itemContext = React.useContext(FormItemContext)
|
||||
const { getFieldState, formState } = useFormContext()
|
||||
const fieldContext = React.useContext(FormFieldContext);
|
||||
const itemContext = React.useContext(FormItemContext);
|
||||
const { getFieldState, formState } = useFormContext();
|
||||
|
||||
const fieldState = getFieldState(fieldContext.name, formState)
|
||||
const fieldState = getFieldState(fieldContext.name, formState);
|
||||
|
||||
if (!fieldContext) {
|
||||
throw new Error("useFormField should be used within <FormField>")
|
||||
}
|
||||
if (!fieldContext) {
|
||||
throw new Error("useFormField should be used within <FormField>");
|
||||
}
|
||||
|
||||
const { id } = itemContext
|
||||
const { id } = itemContext;
|
||||
|
||||
return {
|
||||
id,
|
||||
name: fieldContext.name,
|
||||
formItemId: `${id}-form-item`,
|
||||
formDescriptionId: `${id}-form-item-description`,
|
||||
formMessageId: `${id}-form-item-message`,
|
||||
...fieldState,
|
||||
}
|
||||
}
|
||||
return {
|
||||
id,
|
||||
name: fieldContext.name,
|
||||
formItemId: `${id}-form-item`,
|
||||
formDescriptionId: `${id}-form-item-description`,
|
||||
formMessageId: `${id}-form-item-message`,
|
||||
...fieldState,
|
||||
};
|
||||
};
|
||||
|
||||
type FormItemContextValue = {
|
||||
id: string
|
||||
}
|
||||
id: string;
|
||||
};
|
||||
|
||||
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||
{} as FormItemContextValue
|
||||
)
|
||||
const FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue);
|
||||
|
||||
const FormItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const id = React.useId()
|
||||
const FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => {
|
||||
const id = React.useId();
|
||||
|
||||
return (
|
||||
<FormItemContext.Provider value={{ id }}>
|
||||
<div ref={ref} className={cn("space-y-2", className)} {...props} />
|
||||
</FormItemContext.Provider>
|
||||
)
|
||||
})
|
||||
FormItem.displayName = "FormItem"
|
||||
return (
|
||||
<FormItemContext.Provider value={{ id }}>
|
||||
<div ref={ref} className={cn("space-y-2", className)} {...props} />
|
||||
</FormItemContext.Provider>
|
||||
);
|
||||
},
|
||||
);
|
||||
FormItem.displayName = "FormItem";
|
||||
|
||||
const FormLabel = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { error, formItemId } = useFormField()
|
||||
const { error, formItemId } = useFormField();
|
||||
|
||||
return (
|
||||
<Label
|
||||
ref={ref}
|
||||
className={cn(error && "text-destructive", className)}
|
||||
htmlFor={formItemId}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormLabel.displayName = "FormLabel"
|
||||
return (
|
||||
<Label
|
||||
ref={ref}
|
||||
className={cn(error && "text-destructive", className)}
|
||||
htmlFor={formItemId}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
FormLabel.displayName = "FormLabel";
|
||||
|
||||
const FormControl = React.forwardRef<
|
||||
React.ElementRef<typeof Slot>,
|
||||
React.ComponentPropsWithoutRef<typeof Slot>
|
||||
React.ElementRef<typeof Slot>,
|
||||
React.ComponentPropsWithoutRef<typeof Slot>
|
||||
>(({ ...props }, ref) => {
|
||||
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
||||
const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
|
||||
|
||||
return (
|
||||
<Slot
|
||||
ref={ref}
|
||||
id={formItemId}
|
||||
aria-describedby={
|
||||
!error
|
||||
? `${formDescriptionId}`
|
||||
: `${formDescriptionId} ${formMessageId}`
|
||||
}
|
||||
aria-invalid={!!error}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormControl.displayName = "FormControl"
|
||||
return (
|
||||
<Slot
|
||||
ref={ref}
|
||||
id={formItemId}
|
||||
aria-describedby={
|
||||
!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`
|
||||
}
|
||||
aria-invalid={!!error}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
FormControl.displayName = "FormControl";
|
||||
|
||||
const FormDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { formDescriptionId } = useFormField()
|
||||
const { formDescriptionId } = useFormField();
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formDescriptionId}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormDescription.displayName = "FormDescription"
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formDescriptionId}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
FormDescription.displayName = "FormDescription";
|
||||
|
||||
const FormMessage = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
const { error, formMessageId } = useFormField()
|
||||
const body = error ? String(error?.message) : children
|
||||
const { error, formMessageId } = useFormField();
|
||||
const body = error ? String(error?.message) : children;
|
||||
|
||||
if (!body) {
|
||||
return null
|
||||
}
|
||||
if (!body) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formMessageId}
|
||||
className={cn("text-sm font-medium text-destructive", className)}
|
||||
{...props}
|
||||
>
|
||||
{body}
|
||||
</p>
|
||||
)
|
||||
})
|
||||
FormMessage.displayName = "FormMessage"
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formMessageId}
|
||||
className={cn("text-sm font-medium text-destructive", className)}
|
||||
{...props}
|
||||
>
|
||||
{body}
|
||||
</p>
|
||||
);
|
||||
});
|
||||
FormMessage.displayName = "FormMessage";
|
||||
|
||||
export {
|
||||
useFormField,
|
||||
Form,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormMessage,
|
||||
FormField,
|
||||
}
|
||||
useFormField,
|
||||
Form,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormMessage,
|
||||
FormField,
|
||||
};
|
||||
|
|
|
@ -1,71 +1,71 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import { OTPInput, OTPInputContext } from "input-otp"
|
||||
import { Dot } from "lucide-react"
|
||||
import * as React from "react";
|
||||
import { OTPInput, OTPInputContext } from "input-otp";
|
||||
import { Dot } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const InputOTP = React.forwardRef<
|
||||
React.ElementRef<typeof OTPInput>,
|
||||
React.ComponentPropsWithoutRef<typeof OTPInput>
|
||||
React.ElementRef<typeof OTPInput>,
|
||||
React.ComponentPropsWithoutRef<typeof OTPInput>
|
||||
>(({ className, containerClassName, ...props }, ref) => (
|
||||
<OTPInput
|
||||
ref={ref}
|
||||
containerClassName={cn(
|
||||
"flex items-center gap-2 has-[:disabled]:opacity-50",
|
||||
containerClassName
|
||||
)}
|
||||
className={cn("disabled:cursor-not-allowed", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
InputOTP.displayName = "InputOTP"
|
||||
<OTPInput
|
||||
ref={ref}
|
||||
containerClassName={cn(
|
||||
"flex items-center gap-2 has-[:disabled]:opacity-50",
|
||||
containerClassName,
|
||||
)}
|
||||
className={cn("disabled:cursor-not-allowed", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
InputOTP.displayName = "InputOTP";
|
||||
|
||||
const InputOTPGroup = React.forwardRef<
|
||||
React.ElementRef<"div">,
|
||||
React.ComponentPropsWithoutRef<"div">
|
||||
React.ElementRef<"div">,
|
||||
React.ComponentPropsWithoutRef<"div">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("flex items-center", className)} {...props} />
|
||||
))
|
||||
InputOTPGroup.displayName = "InputOTPGroup"
|
||||
<div ref={ref} className={cn("flex items-center", className)} {...props} />
|
||||
));
|
||||
InputOTPGroup.displayName = "InputOTPGroup";
|
||||
|
||||
const InputOTPSlot = React.forwardRef<
|
||||
React.ElementRef<"div">,
|
||||
React.ComponentPropsWithoutRef<"div"> & { index: number }
|
||||
React.ElementRef<"div">,
|
||||
React.ComponentPropsWithoutRef<"div"> & { index: number }
|
||||
>(({ index, className, ...props }, ref) => {
|
||||
const inputOTPContext = React.useContext(OTPInputContext)
|
||||
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]
|
||||
const inputOTPContext = React.useContext(OTPInputContext);
|
||||
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index];
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
|
||||
isActive && "z-10 ring-2 ring-ring ring-offset-background",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{char}
|
||||
{hasFakeCaret && (
|
||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||
<div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
|
||||
isActive && "z-10 ring-2 ring-ring ring-offset-background",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{char}
|
||||
{hasFakeCaret && (
|
||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||
<div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
InputOTPSlot.displayName = "InputOTPSlot"
|
||||
);
|
||||
});
|
||||
InputOTPSlot.displayName = "InputOTPSlot";
|
||||
|
||||
const InputOTPSeparator = React.forwardRef<
|
||||
React.ElementRef<"div">,
|
||||
React.ComponentPropsWithoutRef<"div">
|
||||
React.ElementRef<"div">,
|
||||
React.ComponentPropsWithoutRef<"div">
|
||||
>(({ ...props }, ref) => (
|
||||
<div ref={ref} role="separator" {...props}>
|
||||
<Dot />
|
||||
</div>
|
||||
))
|
||||
InputOTPSeparator.displayName = "InputOTPSeparator"
|
||||
<div ref={ref} role="separator" {...props}>
|
||||
<Dot />
|
||||
</div>
|
||||
));
|
||||
InputOTPSeparator.displayName = "InputOTPSeparator";
|
||||
|
||||
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }
|
||||
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
|
||||
|
|
|
@ -1,25 +1,24 @@
|
|||
import * as React from "react"
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
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-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Input.displayName = "Input"
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Input.displayName = "Input";
|
||||
|
||||
export { Input }
|
||||
export { Input };
|
||||
|
|
|
@ -1,26 +1,21 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import * as React from "react";
|
||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
)
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
||||
);
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
|
||||
));
|
||||
Label.displayName = LabelPrimitive.Root.displayName;
|
||||
|
||||
export { Label }
|
||||
export { Label };
|
||||
|
|
|
@ -1,236 +1,221 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as MenubarPrimitive from "@radix-ui/react-menubar"
|
||||
import { Check, ChevronRight, Circle } from "lucide-react"
|
||||
import * as React from "react";
|
||||
import * as MenubarPrimitive from "@radix-ui/react-menubar";
|
||||
import { Check, ChevronRight, Circle } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const MenubarMenu = MenubarPrimitive.Menu
|
||||
const MenubarMenu = MenubarPrimitive.Menu;
|
||||
|
||||
const MenubarGroup = MenubarPrimitive.Group
|
||||
const MenubarGroup = MenubarPrimitive.Group;
|
||||
|
||||
const MenubarPortal = MenubarPrimitive.Portal
|
||||
const MenubarPortal = MenubarPrimitive.Portal;
|
||||
|
||||
const MenubarSub = MenubarPrimitive.Sub
|
||||
const MenubarSub = MenubarPrimitive.Sub;
|
||||
|
||||
const MenubarRadioGroup = MenubarPrimitive.RadioGroup
|
||||
const MenubarRadioGroup = MenubarPrimitive.RadioGroup;
|
||||
|
||||
const Menubar = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
|
||||
React.ElementRef<typeof MenubarPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<MenubarPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-10 items-center space-x-1 rounded-md border bg-background p-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Menubar.displayName = MenubarPrimitive.Root.displayName
|
||||
|
||||
const MenubarTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<MenubarPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName
|
||||
|
||||
const MenubarSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<MenubarPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto h-4 w-4" />
|
||||
</MenubarPrimitive.SubTrigger>
|
||||
))
|
||||
MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName
|
||||
|
||||
const MenubarSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<MenubarPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName
|
||||
|
||||
const MenubarContent = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
|
||||
>(
|
||||
(
|
||||
{ className, align = "start", alignOffset = -4, sideOffset = 8, ...props },
|
||||
ref
|
||||
) => (
|
||||
<MenubarPrimitive.Portal>
|
||||
<MenubarPrimitive.Content
|
||||
<MenubarPrimitive.Root
|
||||
ref={ref}
|
||||
align={align}
|
||||
alignOffset={alignOffset}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
"flex h-10 items-center space-x-1 rounded-md border bg-background p-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
/>
|
||||
));
|
||||
Menubar.displayName = MenubarPrimitive.Root.displayName;
|
||||
|
||||
const MenubarTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<MenubarPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName;
|
||||
|
||||
const MenubarSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<MenubarPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto h-4 w-4" />
|
||||
</MenubarPrimitive.SubTrigger>
|
||||
));
|
||||
MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName;
|
||||
|
||||
const MenubarSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<MenubarPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName;
|
||||
|
||||
const MenubarContent = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
|
||||
>(({ className, align = "start", alignOffset = -4, sideOffset = 8, ...props }, ref) => (
|
||||
<MenubarPrimitive.Portal>
|
||||
<MenubarPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
alignOffset={alignOffset}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</MenubarPrimitive.Portal>
|
||||
)
|
||||
)
|
||||
MenubarContent.displayName = MenubarPrimitive.Content.displayName
|
||||
));
|
||||
MenubarContent.displayName = MenubarPrimitive.Content.displayName;
|
||||
|
||||
const MenubarItem = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
}
|
||||
React.ElementRef<typeof MenubarPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<MenubarPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
MenubarItem.displayName = MenubarPrimitive.Item.displayName
|
||||
<MenubarPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
MenubarItem.displayName = MenubarPrimitive.Item.displayName;
|
||||
|
||||
const MenubarCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
|
||||
React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<MenubarPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<MenubarPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</MenubarPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</MenubarPrimitive.CheckboxItem>
|
||||
))
|
||||
MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName
|
||||
<MenubarPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<MenubarPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</MenubarPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</MenubarPrimitive.CheckboxItem>
|
||||
));
|
||||
MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName;
|
||||
|
||||
const MenubarRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
|
||||
React.ElementRef<typeof MenubarPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<MenubarPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<MenubarPrimitive.ItemIndicator>
|
||||
<Circle className="h-2 w-2 fill-current" />
|
||||
</MenubarPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</MenubarPrimitive.RadioItem>
|
||||
))
|
||||
MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName
|
||||
<MenubarPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<MenubarPrimitive.ItemIndicator>
|
||||
<Circle className="h-2 w-2 fill-current" />
|
||||
</MenubarPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</MenubarPrimitive.RadioItem>
|
||||
));
|
||||
MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName;
|
||||
|
||||
const MenubarLabel = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
}
|
||||
React.ElementRef<typeof MenubarPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<MenubarPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
MenubarLabel.displayName = MenubarPrimitive.Label.displayName
|
||||
<MenubarPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
MenubarLabel.displayName = MenubarPrimitive.Label.displayName;
|
||||
|
||||
const MenubarSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
|
||||
React.ElementRef<typeof MenubarPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<MenubarPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName
|
||||
|
||||
const MenubarShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"ml-auto text-xs tracking-widest text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
<MenubarPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
MenubarShortcut.displayname = "MenubarShortcut"
|
||||
));
|
||||
MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName;
|
||||
|
||||
const MenubarShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
MenubarShortcut.displayname = "MenubarShortcut";
|
||||
|
||||
export {
|
||||
Menubar,
|
||||
MenubarMenu,
|
||||
MenubarTrigger,
|
||||
MenubarContent,
|
||||
MenubarItem,
|
||||
MenubarSeparator,
|
||||
MenubarLabel,
|
||||
MenubarCheckboxItem,
|
||||
MenubarRadioGroup,
|
||||
MenubarRadioItem,
|
||||
MenubarPortal,
|
||||
MenubarSubContent,
|
||||
MenubarSubTrigger,
|
||||
MenubarGroup,
|
||||
MenubarSub,
|
||||
MenubarShortcut,
|
||||
}
|
||||
Menubar,
|
||||
MenubarMenu,
|
||||
MenubarTrigger,
|
||||
MenubarContent,
|
||||
MenubarItem,
|
||||
MenubarSeparator,
|
||||
MenubarLabel,
|
||||
MenubarCheckboxItem,
|
||||
MenubarRadioGroup,
|
||||
MenubarRadioItem,
|
||||
MenubarPortal,
|
||||
MenubarSubContent,
|
||||
MenubarSubTrigger,
|
||||
MenubarGroup,
|
||||
MenubarSub,
|
||||
MenubarShortcut,
|
||||
};
|
||||
|
|
|
@ -1,128 +1,123 @@
|
|||
import * as React from "react"
|
||||
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
|
||||
import { cva } from "class-variance-authority"
|
||||
import { ChevronDown } from "lucide-react"
|
||||
import * as React from "react";
|
||||
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
|
||||
import { cva } from "class-variance-authority";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const NavigationMenu = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-10 flex max-w-max flex-1 items-center justify-center",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<NavigationMenuViewport />
|
||||
</NavigationMenuPrimitive.Root>
|
||||
))
|
||||
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
|
||||
<NavigationMenuPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("relative z-10 flex max-w-max flex-1 items-center justify-center", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<NavigationMenuViewport />
|
||||
</NavigationMenuPrimitive.Root>
|
||||
));
|
||||
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName;
|
||||
|
||||
const NavigationMenuList = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"group flex flex-1 list-none items-center justify-center space-x-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
|
||||
<NavigationMenuPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"group flex flex-1 list-none items-center justify-center space-x-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;
|
||||
|
||||
const NavigationMenuItem = NavigationMenuPrimitive.Item
|
||||
const NavigationMenuItem = NavigationMenuPrimitive.Item;
|
||||
|
||||
const navigationMenuTriggerStyle = cva(
|
||||
"group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50"
|
||||
)
|
||||
"group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50",
|
||||
);
|
||||
|
||||
const NavigationMenuTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}{" "}
|
||||
<ChevronDown
|
||||
className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</NavigationMenuPrimitive.Trigger>
|
||||
))
|
||||
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
|
||||
<NavigationMenuPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}{" "}
|
||||
<ChevronDown
|
||||
className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</NavigationMenuPrimitive.Trigger>
|
||||
));
|
||||
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName;
|
||||
|
||||
const NavigationMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
|
||||
<NavigationMenuPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName;
|
||||
|
||||
const NavigationMenuLink = NavigationMenuPrimitive.Link
|
||||
const NavigationMenuLink = NavigationMenuPrimitive.Link;
|
||||
|
||||
const NavigationMenuViewport = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className={cn("absolute left-0 top-full flex justify-center")}>
|
||||
<NavigationMenuPrimitive.Viewport
|
||||
className={cn(
|
||||
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
NavigationMenuViewport.displayName =
|
||||
NavigationMenuPrimitive.Viewport.displayName
|
||||
<div className={cn("absolute left-0 top-full flex justify-center")}>
|
||||
<NavigationMenuPrimitive.Viewport
|
||||
className={cn(
|
||||
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
NavigationMenuViewport.displayName = NavigationMenuPrimitive.Viewport.displayName;
|
||||
|
||||
const NavigationMenuIndicator = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.Indicator
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
|
||||
</NavigationMenuPrimitive.Indicator>
|
||||
))
|
||||
NavigationMenuIndicator.displayName =
|
||||
NavigationMenuPrimitive.Indicator.displayName
|
||||
<NavigationMenuPrimitive.Indicator
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
|
||||
</NavigationMenuPrimitive.Indicator>
|
||||
));
|
||||
NavigationMenuIndicator.displayName = NavigationMenuPrimitive.Indicator.displayName;
|
||||
|
||||
export {
|
||||
navigationMenuTriggerStyle,
|
||||
NavigationMenu,
|
||||
NavigationMenuList,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuContent,
|
||||
NavigationMenuTrigger,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuIndicator,
|
||||
NavigationMenuViewport,
|
||||
}
|
||||
navigationMenuTriggerStyle,
|
||||
NavigationMenu,
|
||||
NavigationMenuList,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuContent,
|
||||
NavigationMenuTrigger,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuIndicator,
|
||||
NavigationMenuViewport,
|
||||
};
|
||||
|
|
|
@ -1,31 +1,31 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||
import * as React from "react";
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Popover = PopoverPrimitive.Root
|
||||
const Popover = PopoverPrimitive.Root;
|
||||
|
||||
const PopoverTrigger = PopoverPrimitive.Trigger
|
||||
const PopoverTrigger = PopoverPrimitive.Trigger;
|
||||
|
||||
const PopoverContent = React.forwardRef<
|
||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
))
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
));
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent }
|
||||
export { Popover, PopoverTrigger, PopoverContent };
|
||||
|
|
|
@ -1,32 +1,31 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
||||
import * as React from "react";
|
||||
import * as ProgressPrimitive from "@radix-ui/react-progress";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface ProgressProps extends React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> {
|
||||
indicatorColor?: string
|
||||
indicatorColor?: string;
|
||||
}
|
||||
|
||||
const Progress = React.forwardRef<
|
||||
React.ElementRef<typeof ProgressPrimitive.Root>,
|
||||
ProgressProps
|
||||
>(({ className, value, indicatorColor, ...props }, ref) => (
|
||||
<ProgressPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
className={`h-full w-full flex-1 bg-primary transition-all ${indicatorColor}`}
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
))
|
||||
Progress.displayName = ProgressPrimitive.Root.displayName
|
||||
const Progress = React.forwardRef<React.ElementRef<typeof ProgressPrimitive.Root>, ProgressProps>(
|
||||
({ className, value, indicatorColor, ...props }, ref) => (
|
||||
<ProgressPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
className={`h-full w-full flex-1 bg-primary transition-all ${indicatorColor}`}
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
),
|
||||
);
|
||||
Progress.displayName = ProgressPrimitive.Root.displayName;
|
||||
|
||||
export { Progress }
|
||||
export { Progress };
|
||||
|
|
|
@ -1,48 +1,46 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
||||
import * as React from "react";
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const ScrollArea = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("relative overflow-hidden", className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
))
|
||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
|
||||
<ScrollAreaPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("relative overflow-hidden", className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
));
|
||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
|
||||
|
||||
const ScrollBar = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
>(({ className, orientation = "vertical", ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
ref={ref}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none select-none transition-colors",
|
||||
orientation === "vertical" &&
|
||||
"h-full w-2.5 border-l border-l-transparent p-[1px]",
|
||||
orientation === "horizontal" &&
|
||||
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
))
|
||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
ref={ref}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none select-none transition-colors",
|
||||
orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent p-[1px]",
|
||||
orientation === "horizontal" && "h-2.5 flex-col border-t border-t-transparent p-[1px]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
));
|
||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
|
||||
|
||||
export { ScrollArea, ScrollBar }
|
||||
export { ScrollArea, ScrollBar };
|
||||
|
|
|
@ -1,160 +1,153 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
||||
import * as React from "react";
|
||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||
import { Check, ChevronDown, ChevronUp } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Select = SelectPrimitive.Root
|
||||
const Select = SelectPrimitive.Root;
|
||||
|
||||
const SelectGroup = SelectPrimitive.Group
|
||||
const SelectGroup = SelectPrimitive.Group;
|
||||
|
||||
const SelectValue = SelectPrimitive.Value
|
||||
const SelectValue = SelectPrimitive.Value;
|
||||
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
))
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
));
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
||||
|
||||
const SelectScrollUpButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
))
|
||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
ref={ref}
|
||||
className={cn("flex cursor-default items-center justify-center py-1", className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
));
|
||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
|
||||
|
||||
const SelectScrollDownButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
))
|
||||
SelectScrollDownButton.displayName =
|
||||
SelectPrimitive.ScrollDownButton.displayName
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
ref={ref}
|
||||
className={cn("flex cursor-default items-center justify-center py-1", className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
));
|
||||
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;
|
||||
|
||||
const SelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
>(({ className, children, position = "popper", ...props }, ref) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
))
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className,
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
));
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
||||
|
||||
const SelectLabel = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName;
|
||||
|
||||
const SelectItem = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
))
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
));
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
||||
|
||||
const SelectSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton,
|
||||
}
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton,
|
||||
};
|
||||
|
|
|
@ -1,140 +1,123 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as SheetPrimitive from "@radix-ui/react-dialog"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { X } from "lucide-react"
|
||||
import * as React from "react";
|
||||
import * as SheetPrimitive from "@radix-ui/react-dialog";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Sheet = SheetPrimitive.Root
|
||||
const Sheet = SheetPrimitive.Root;
|
||||
|
||||
const SheetTrigger = SheetPrimitive.Trigger
|
||||
const SheetTrigger = SheetPrimitive.Trigger;
|
||||
|
||||
const SheetClose = SheetPrimitive.Close
|
||||
const SheetClose = SheetPrimitive.Close;
|
||||
|
||||
const SheetPortal = SheetPrimitive.Portal
|
||||
const SheetPortal = SheetPrimitive.Portal;
|
||||
|
||||
const SheetOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
||||
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Overlay
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
))
|
||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
|
||||
<SheetPrimitive.Overlay
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
));
|
||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
|
||||
|
||||
const sheetVariants = cva(
|
||||
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
||||
{
|
||||
variants: {
|
||||
side: {
|
||||
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
||||
bottom:
|
||||
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
||||
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
|
||||
right:
|
||||
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
|
||||
},
|
||||
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
||||
{
|
||||
variants: {
|
||||
side: {
|
||||
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
||||
bottom: "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
||||
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
|
||||
right: "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
side: "right",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
side: "right",
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
interface SheetContentProps
|
||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||
VariantProps<typeof sheetVariants> {}
|
||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||
VariantProps<typeof sheetVariants> {}
|
||||
|
||||
const SheetContent = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Content>,
|
||||
SheetContentProps
|
||||
React.ElementRef<typeof SheetPrimitive.Content>,
|
||||
SheetContentProps
|
||||
>(({ side = "right", className, children, ...props }, ref) => (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(sheetVariants({ side }), className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SheetPrimitive.Close 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-secondary">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
))
|
||||
SheetContent.displayName = SheetPrimitive.Content.displayName
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(sheetVariants({ side }), className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SheetPrimitive.Close 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-secondary">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
));
|
||||
SheetContent.displayName = SheetPrimitive.Content.displayName;
|
||||
|
||||
const SheetHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-2 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
SheetHeader.displayName = "SheetHeader"
|
||||
const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
|
||||
);
|
||||
SheetHeader.displayName = "SheetHeader";
|
||||
|
||||
const SheetFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
SheetFooter.displayName = "SheetFooter"
|
||||
const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
SheetFooter.displayName = "SheetFooter";
|
||||
|
||||
const SheetTitle = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
||||
React.ElementRef<typeof SheetPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-lg font-semibold text-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SheetTitle.displayName = SheetPrimitive.Title.displayName
|
||||
<SheetPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-lg font-semibold text-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SheetTitle.displayName = SheetPrimitive.Title.displayName;
|
||||
|
||||
const SheetDescription = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
||||
React.ElementRef<typeof SheetPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SheetDescription.displayName = SheetPrimitive.Description.displayName
|
||||
<SheetPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SheetDescription.displayName = SheetPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetPortal,
|
||||
SheetOverlay,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
}
|
||||
Sheet,
|
||||
SheetPortal,
|
||||
SheetOverlay,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
};
|
||||
|
|
|
@ -1,117 +1,95 @@
|
|||
import * as React from "react"
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Table = React.forwardRef<
|
||||
HTMLTableElement,
|
||||
React.HTMLAttributes<HTMLTableElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="relative w-full overflow-auto">
|
||||
<table
|
||||
ref={ref}
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
Table.displayName = "Table"
|
||||
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div className="relative w-full overflow-auto">
|
||||
<table
|
||||
ref={ref}
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
);
|
||||
Table.displayName = "Table";
|
||||
|
||||
const TableHeader = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
||||
))
|
||||
TableHeader.displayName = "TableHeader"
|
||||
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
||||
));
|
||||
TableHeader.displayName = "TableHeader";
|
||||
|
||||
const TableBody = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tbody
|
||||
ref={ref}
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableBody.displayName = "TableBody"
|
||||
<tbody ref={ref} className={cn("[&_tr:last-child]:border-0", className)} {...props} />
|
||||
));
|
||||
TableBody.displayName = "TableBody";
|
||||
|
||||
const TableFooter = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tfoot
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableFooter.displayName = "TableFooter"
|
||||
<tfoot
|
||||
ref={ref}
|
||||
className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableFooter.displayName = "TableFooter";
|
||||
|
||||
const TableRow = React.forwardRef<
|
||||
HTMLTableRowElement,
|
||||
React.HTMLAttributes<HTMLTableRowElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableRow.displayName = "TableRow"
|
||||
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
TableRow.displayName = "TableRow";
|
||||
|
||||
const TableHead = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||
HTMLTableCellElement,
|
||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<th
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableHead.displayName = "TableHead"
|
||||
<th
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableHead.displayName = "TableHead";
|
||||
|
||||
const TableCell = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||
HTMLTableCellElement,
|
||||
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableCell.displayName = "TableCell"
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableCell.displayName = "TableCell";
|
||||
|
||||
const TableCaption = React.forwardRef<
|
||||
HTMLTableCaptionElement,
|
||||
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||
HTMLTableCaptionElement,
|
||||
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<caption
|
||||
ref={ref}
|
||||
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableCaption.displayName = "TableCaption"
|
||||
<caption ref={ref} className={cn("mt-4 text-sm text-muted-foreground", className)} {...props} />
|
||||
));
|
||||
TableCaption.displayName = "TableCaption";
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
}
|
||||
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption };
|
||||
|
|
|
@ -1,24 +1,23 @@
|
|||
import * as React from "react"
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface TextareaProps
|
||||
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
||||
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
||||
|
||||
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Textarea.displayName = "Textarea"
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Textarea.displayName = "Textarea";
|
||||
|
||||
export { Textarea }
|
||||
export { Textarea };
|
||||
|
|
|
@ -1,129 +1,128 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as ToastPrimitives from "@radix-ui/react-toast"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { X } from "lucide-react"
|
||||
import * as React from "react";
|
||||
import * as ToastPrimitives from "@radix-ui/react-toast";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const ToastProvider = ToastPrimitives.Provider
|
||||
const ToastProvider = ToastPrimitives.Provider;
|
||||
|
||||
const ToastViewport = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
||||
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Viewport
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
|
||||
<ToastPrimitives.Viewport
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
|
||||
|
||||
const toastVariants = cva(
|
||||
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "border bg-background text-foreground",
|
||||
destructive:
|
||||
"destructive group border-destructive bg-destructive text-destructive-foreground",
|
||||
},
|
||||
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "border bg-background text-foreground",
|
||||
destructive:
|
||||
"destructive group border-destructive bg-destructive text-destructive-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const Toast = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
||||
VariantProps<typeof toastVariants>
|
||||
React.ElementRef<typeof ToastPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> & VariantProps<typeof toastVariants>
|
||||
>(({ className, variant, ...props }, ref) => {
|
||||
return (
|
||||
<ToastPrimitives.Root
|
||||
ref={ref}
|
||||
className={cn(toastVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Toast.displayName = ToastPrimitives.Root.displayName
|
||||
return (
|
||||
<ToastPrimitives.Root
|
||||
ref={ref}
|
||||
className={cn(toastVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
Toast.displayName = ToastPrimitives.Root.displayName;
|
||||
|
||||
const ToastAction = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
||||
React.ElementRef<typeof ToastPrimitives.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Action
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastAction.displayName = ToastPrimitives.Action.displayName
|
||||
<ToastPrimitives.Action
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToastAction.displayName = ToastPrimitives.Action.displayName;
|
||||
|
||||
const ToastClose = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Close>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
||||
React.ElementRef<typeof ToastPrimitives.Close>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Close
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
|
||||
className
|
||||
)}
|
||||
toast-close=""
|
||||
{...props}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</ToastPrimitives.Close>
|
||||
))
|
||||
ToastClose.displayName = ToastPrimitives.Close.displayName
|
||||
<ToastPrimitives.Close
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
|
||||
className,
|
||||
)}
|
||||
toast-close=""
|
||||
{...props}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</ToastPrimitives.Close>
|
||||
));
|
||||
ToastClose.displayName = ToastPrimitives.Close.displayName;
|
||||
|
||||
const ToastTitle = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
||||
React.ElementRef<typeof ToastPrimitives.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Title
|
||||
ref={ref}
|
||||
className={cn("text-sm font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastTitle.displayName = ToastPrimitives.Title.displayName
|
||||
<ToastPrimitives.Title
|
||||
ref={ref}
|
||||
className={cn("text-sm font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToastTitle.displayName = ToastPrimitives.Title.displayName;
|
||||
|
||||
const ToastDescription = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
||||
React.ElementRef<typeof ToastPrimitives.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm opacity-90", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastDescription.displayName = ToastPrimitives.Description.displayName
|
||||
<ToastPrimitives.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm opacity-90", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToastDescription.displayName = ToastPrimitives.Description.displayName;
|
||||
|
||||
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
|
||||
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
|
||||
|
||||
type ToastActionElement = React.ReactElement<typeof ToastAction>
|
||||
type ToastActionElement = React.ReactElement<typeof ToastAction>;
|
||||
|
||||
export {
|
||||
type ToastProps,
|
||||
type ToastActionElement,
|
||||
ToastProvider,
|
||||
ToastViewport,
|
||||
Toast,
|
||||
ToastTitle,
|
||||
ToastDescription,
|
||||
ToastClose,
|
||||
ToastAction,
|
||||
}
|
||||
type ToastProps,
|
||||
type ToastActionElement,
|
||||
ToastProvider,
|
||||
ToastViewport,
|
||||
Toast,
|
||||
ToastTitle,
|
||||
ToastDescription,
|
||||
ToastClose,
|
||||
ToastAction,
|
||||
};
|
||||
|
|
|
@ -1,35 +1,33 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Toast,
|
||||
ToastClose,
|
||||
ToastDescription,
|
||||
ToastProvider,
|
||||
ToastTitle,
|
||||
ToastViewport,
|
||||
} from "@/components/ui/toast"
|
||||
import { useToast } from "@/components/ui/use-toast"
|
||||
Toast,
|
||||
ToastClose,
|
||||
ToastDescription,
|
||||
ToastProvider,
|
||||
ToastTitle,
|
||||
ToastViewport,
|
||||
} from "@/components/ui/toast";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
|
||||
export function Toaster() {
|
||||
const { toasts } = useToast()
|
||||
const { toasts } = useToast();
|
||||
|
||||
return (
|
||||
<ToastProvider>
|
||||
{toasts.map(function ({ id, title, description, action, ...props }) {
|
||||
return (
|
||||
<Toast key={id} {...props}>
|
||||
<div className="grid gap-1">
|
||||
{title && <ToastTitle>{title}</ToastTitle>}
|
||||
{description && (
|
||||
<ToastDescription>{description}</ToastDescription>
|
||||
)}
|
||||
</div>
|
||||
{action}
|
||||
<ToastClose />
|
||||
</Toast>
|
||||
)
|
||||
})}
|
||||
<ToastViewport />
|
||||
</ToastProvider>
|
||||
)
|
||||
return (
|
||||
<ToastProvider>
|
||||
{toasts.map(function ({ id, title, description, action, ...props }) {
|
||||
return (
|
||||
<Toast key={id} {...props}>
|
||||
<div className="grid gap-1">
|
||||
{title && <ToastTitle>{title}</ToastTitle>}
|
||||
{description && <ToastDescription>{description}</ToastDescription>}
|
||||
</div>
|
||||
{action}
|
||||
<ToastClose />
|
||||
</Toast>
|
||||
);
|
||||
})}
|
||||
<ToastViewport />
|
||||
</ToastProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,45 +1,45 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as TogglePrimitive from "@radix-ui/react-toggle"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import * as React from "react";
|
||||
import * as TogglePrimitive from "@radix-ui/react-toggle";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const toggleVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-transparent",
|
||||
outline:
|
||||
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-3",
|
||||
sm: "h-9 px-2.5",
|
||||
lg: "h-11 px-5",
|
||||
},
|
||||
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-transparent",
|
||||
outline:
|
||||
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-3",
|
||||
sm: "h-9 px-2.5",
|
||||
lg: "h-11 px-5",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const Toggle = React.forwardRef<
|
||||
React.ElementRef<typeof TogglePrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants>
|
||||
React.ElementRef<typeof TogglePrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants>
|
||||
>(({ className, variant, size, ...props }, ref) => (
|
||||
<TogglePrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(toggleVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
<TogglePrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(toggleVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
Toggle.displayName = TogglePrimitive.Root.displayName
|
||||
Toggle.displayName = TogglePrimitive.Root.displayName;
|
||||
|
||||
export { Toggle, toggleVariants }
|
||||
export { Toggle, toggleVariants };
|
||||
|
|
|
@ -1,30 +1,30 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||
import * as React from "react";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider
|
||||
const TooltipProvider = TooltipPrimitive.Provider;
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root
|
||||
const Tooltip = TooltipPrimitive.Root;
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||
|
|
|
@ -1,194 +1,191 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
// Inspired by react-hot-toast library
|
||||
import * as React from "react"
|
||||
import * as React from "react";
|
||||
|
||||
import type {
|
||||
ToastActionElement,
|
||||
ToastProps,
|
||||
} from "@/components/ui/toast"
|
||||
import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
|
||||
|
||||
const TOAST_LIMIT = 1
|
||||
const TOAST_REMOVE_DELAY = 1000000
|
||||
const TOAST_LIMIT = 1;
|
||||
const TOAST_REMOVE_DELAY = 1000000;
|
||||
|
||||
type ToasterToast = ToastProps & {
|
||||
id: string
|
||||
title?: React.ReactNode
|
||||
description?: React.ReactNode
|
||||
action?: ToastActionElement
|
||||
}
|
||||
id: string;
|
||||
title?: React.ReactNode;
|
||||
description?: React.ReactNode;
|
||||
action?: ToastActionElement;
|
||||
};
|
||||
|
||||
const actionTypes = {
|
||||
ADD_TOAST: "ADD_TOAST",
|
||||
UPDATE_TOAST: "UPDATE_TOAST",
|
||||
DISMISS_TOAST: "DISMISS_TOAST",
|
||||
REMOVE_TOAST: "REMOVE_TOAST",
|
||||
} as const
|
||||
ADD_TOAST: "ADD_TOAST",
|
||||
UPDATE_TOAST: "UPDATE_TOAST",
|
||||
DISMISS_TOAST: "DISMISS_TOAST",
|
||||
REMOVE_TOAST: "REMOVE_TOAST",
|
||||
} as const;
|
||||
|
||||
let count = 0
|
||||
let count = 0;
|
||||
|
||||
function genId() {
|
||||
count = (count + 1) % Number.MAX_SAFE_INTEGER
|
||||
return count.toString()
|
||||
count = (count + 1) % Number.MAX_SAFE_INTEGER;
|
||||
return count.toString();
|
||||
}
|
||||
|
||||
type ActionType = typeof actionTypes
|
||||
type ActionType = typeof actionTypes;
|
||||
|
||||
type Action =
|
||||
| {
|
||||
type: ActionType["ADD_TOAST"]
|
||||
toast: ToasterToast
|
||||
}
|
||||
| {
|
||||
type: ActionType["UPDATE_TOAST"]
|
||||
toast: Partial<ToasterToast>
|
||||
}
|
||||
| {
|
||||
type: ActionType["DISMISS_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
| {
|
||||
type: ActionType["REMOVE_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
| {
|
||||
type: ActionType["ADD_TOAST"];
|
||||
toast: ToasterToast;
|
||||
}
|
||||
| {
|
||||
type: ActionType["UPDATE_TOAST"];
|
||||
toast: Partial<ToasterToast>;
|
||||
}
|
||||
| {
|
||||
type: ActionType["DISMISS_TOAST"];
|
||||
toastId?: ToasterToast["id"];
|
||||
}
|
||||
| {
|
||||
type: ActionType["REMOVE_TOAST"];
|
||||
toastId?: ToasterToast["id"];
|
||||
};
|
||||
|
||||
interface State {
|
||||
toasts: ToasterToast[]
|
||||
toasts: ToasterToast[];
|
||||
}
|
||||
|
||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
|
||||
|
||||
const addToRemoveQueue = (toastId: string) => {
|
||||
if (toastTimeouts.has(toastId)) {
|
||||
return
|
||||
}
|
||||
if (toastTimeouts.has(toastId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
toastTimeouts.delete(toastId)
|
||||
dispatch({
|
||||
type: "REMOVE_TOAST",
|
||||
toastId: toastId,
|
||||
})
|
||||
}, TOAST_REMOVE_DELAY)
|
||||
const timeout = setTimeout(() => {
|
||||
toastTimeouts.delete(toastId);
|
||||
dispatch({
|
||||
type: "REMOVE_TOAST",
|
||||
toastId: toastId,
|
||||
});
|
||||
}, TOAST_REMOVE_DELAY);
|
||||
|
||||
toastTimeouts.set(toastId, timeout)
|
||||
}
|
||||
toastTimeouts.set(toastId, timeout);
|
||||
};
|
||||
|
||||
export const reducer = (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case "ADD_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||
}
|
||||
switch (action.type) {
|
||||
case "ADD_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||
};
|
||||
|
||||
case "UPDATE_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === action.toast.id ? { ...t, ...action.toast } : t
|
||||
),
|
||||
}
|
||||
case "UPDATE_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === action.toast.id ? { ...t, ...action.toast } : t,
|
||||
),
|
||||
};
|
||||
|
||||
case "DISMISS_TOAST": {
|
||||
const { toastId } = action
|
||||
case "DISMISS_TOAST": {
|
||||
const { toastId } = action;
|
||||
|
||||
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||
// but I'll keep it here for simplicity
|
||||
if (toastId) {
|
||||
addToRemoveQueue(toastId)
|
||||
} else {
|
||||
state.toasts.forEach((toast) => {
|
||||
addToRemoveQueue(toast.id)
|
||||
})
|
||||
}
|
||||
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||
// but I'll keep it here for simplicity
|
||||
if (toastId) {
|
||||
addToRemoveQueue(toastId);
|
||||
} else {
|
||||
state.toasts.forEach((toast) => {
|
||||
addToRemoveQueue(toast.id);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === toastId || toastId === undefined
|
||||
? {
|
||||
...t,
|
||||
open: false,
|
||||
}
|
||||
: t
|
||||
),
|
||||
}
|
||||
}
|
||||
case "REMOVE_TOAST":
|
||||
if (action.toastId === undefined) {
|
||||
return {
|
||||
...state,
|
||||
toasts: [],
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === toastId || toastId === undefined
|
||||
? {
|
||||
...t,
|
||||
open: false,
|
||||
}
|
||||
: t,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||
}
|
||||
}
|
||||
}
|
||||
case "REMOVE_TOAST":
|
||||
if (action.toastId === undefined) {
|
||||
return {
|
||||
...state,
|
||||
toasts: [],
|
||||
};
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const listeners: Array<(state: State) => void> = []
|
||||
const listeners: Array<(state: State) => void> = [];
|
||||
|
||||
let memoryState: State = { toasts: [] }
|
||||
let memoryState: State = { toasts: [] };
|
||||
|
||||
function dispatch(action: Action) {
|
||||
memoryState = reducer(memoryState, action)
|
||||
listeners.forEach((listener) => {
|
||||
listener(memoryState)
|
||||
})
|
||||
memoryState = reducer(memoryState, action);
|
||||
listeners.forEach((listener) => {
|
||||
listener(memoryState);
|
||||
});
|
||||
}
|
||||
|
||||
type Toast = Omit<ToasterToast, "id">
|
||||
type Toast = Omit<ToasterToast, "id">;
|
||||
|
||||
function toast({ ...props }: Toast) {
|
||||
const id = genId()
|
||||
const id = genId();
|
||||
|
||||
const update = (props: ToasterToast) =>
|
||||
dispatch({
|
||||
type: "UPDATE_TOAST",
|
||||
toast: { ...props, id },
|
||||
});
|
||||
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
|
||||
|
||||
const update = (props: ToasterToast) =>
|
||||
dispatch({
|
||||
type: "UPDATE_TOAST",
|
||||
toast: { ...props, id },
|
||||
})
|
||||
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
|
||||
type: "ADD_TOAST",
|
||||
toast: {
|
||||
...props,
|
||||
id,
|
||||
open: true,
|
||||
onOpenChange: (open) => {
|
||||
if (!open) dismiss();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: "ADD_TOAST",
|
||||
toast: {
|
||||
...props,
|
||||
id,
|
||||
open: true,
|
||||
onOpenChange: (open) => {
|
||||
if (!open) dismiss()
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
id: id,
|
||||
dismiss,
|
||||
update,
|
||||
}
|
||||
return {
|
||||
id: id,
|
||||
dismiss,
|
||||
update,
|
||||
};
|
||||
}
|
||||
|
||||
function useToast() {
|
||||
const [state, setState] = React.useState<State>(memoryState)
|
||||
const [state, setState] = React.useState<State>(memoryState);
|
||||
|
||||
React.useEffect(() => {
|
||||
listeners.push(setState)
|
||||
return () => {
|
||||
const index = listeners.indexOf(setState)
|
||||
if (index > -1) {
|
||||
listeners.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}, [state])
|
||||
React.useEffect(() => {
|
||||
listeners.push(setState);
|
||||
return () => {
|
||||
const index = listeners.indexOf(setState);
|
||||
if (index > -1) {
|
||||
listeners.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}, [state]);
|
||||
|
||||
return {
|
||||
...state,
|
||||
toast,
|
||||
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
toast,
|
||||
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
||||
};
|
||||
}
|
||||
|
||||
export { useToast, toast }
|
||||
export { useToast, toast };
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue