mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-28 05:25:33 +00:00
feat: added and fixed query and search interface
This commit is contained in:
parent
b8e070c1ac
commit
dc7e35e76a
7 changed files with 447 additions and 0 deletions
BIN
apps/web/public/logo.png
Normal file
BIN
apps/web/public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 273 KiB |
1
apps/web/public/placeholder.svg
Normal file
1
apps/web/public/placeholder.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="299" height="186" fill="none"><g clip-path="url(#a)"><rect width="299" height="186" fill="#292B2B" rx="3"/><g stroke="#C9C9C9" opacity=".5"><path d="m71.594 205.646 224-224M95.173 205.646l224-224M118.752 205.646l224-224M142.331 205.646l224-224M165.91 205.646l224-224M189.489 205.646l224-224M213.067 205.646l224-224M236.646 205.646l224-224M260.226 205.646l224-224M283.804 205.646l224-224M-211.354 205.646l224-224M-187.774 205.646l223.999-224M-164.196 205.646l224-224M-140.617 205.646l224-224M-117.038 205.646l224-224M-93.459 205.646l224-224M-69.88 205.646l224-224M-46.301 205.646l224-224M-22.722 205.646l224-224M.857 205.646l224-224M24.436 205.646l224-224M48.015 205.646l224-224M272.723 205.646l-224-224M249.144 205.646l-224-224M225.564 205.646l-224-224M201.985 205.646l-224-224M178.407 205.646l-224-224M154.828 205.646l-224-224M131.249 205.646l-224-224M107.67 205.646l-224-224M84.09 205.646l-224-224M60.511 205.646l-224-224M36.933 205.646l-224-224M13.354 205.646l-224-224M508.512 205.646l-224-224M484.933 205.646l-224-224M461.354 205.646l-224-224M437.775 205.646l-224-224M414.196 205.646l-224-224M390.617 205.646l-224-224M367.038 205.646l-224-224M343.459 205.646l-224-224M319.88 205.646l-224-224M296.3 205.646l-224-224"/></g><g stroke="#C9C9C9" opacity=".5"><path d="m83.594 205.646 224-224M107.173 205.646l224-224M130.752 205.646l224-224M154.331 205.646l224-224M177.91 205.646l224-224M201.489 205.646l224-224M225.067 205.646l224-224M248.646 205.646l224-224M272.226 205.646l224-224M295.804 205.646l224-224M-199.354 205.646l224-224M-175.774 205.646l223.999-224M-152.196 205.646l224-224M-128.617 205.646l224-224M-105.038 205.646l224-224M-81.459 205.646l224-224M-57.88 205.646l224-224M-34.301 205.646l224-224M-10.722 205.646l224-224M12.857 205.646l224-224M36.436 205.646l224-224M60.015 205.646l224-224M284.723 205.646l-224-224M261.144 205.646l-224-224M237.564 205.646l-224-224M213.985 205.646l-224-224M190.407 205.646l-224-224M166.828 205.646l-224-224M143.249 205.646l-224-224M119.67 205.646l-224-224M96.09 205.646l-224-224M72.511 205.646l-224-224M48.933 205.646l-224-224M25.354 205.646l-224-224M520.512 205.646l-224-224M496.933 205.646l-224-224M473.354 205.646l-224-224M449.775 205.646l-224-224M426.196 205.646l-224-224M402.617 205.646l-224-224M379.038 205.646l-224-224M355.459 205.646l-224-224M331.88 205.646l-224-224M308.3 205.646l-224-224"/></g></g><defs><clipPath id="a"><rect width="299" height="186" fill="#fff" rx="3"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
48
apps/web/src/app/api/ask/route.ts
Normal file
48
apps/web/src/app/api/ask/route.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { db } from "@/server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { sessions, users } from "@/server/db/schema";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { env } from "@/env";
|
||||
|
||||
export const runtime = "edge";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const token = req.cookies.get("next-auth.session-token")?.value ?? req.cookies.get("__Secure-authjs.session-token")?.value ?? req.cookies.get("authjs.session-token")?.value ?? req.headers.get("Authorization")?.replace("Bearer ", "");
|
||||
|
||||
const sessionData = await db.select().from(sessions).where(eq(sessions.sessionToken, token!))
|
||||
|
||||
if (!sessionData || sessionData.length === 0) {
|
||||
return new Response(JSON.stringify({ message: "Invalid Key, session not found." }), { status: 404 });
|
||||
}
|
||||
|
||||
const user = await db.select().from(users).where(eq(users.id, sessionData[0].userId)).limit(1)
|
||||
|
||||
if (!user || user.length === 0) {
|
||||
return NextResponse.json({ message: "Invalid Key, session not found." }, { status: 404 });
|
||||
}
|
||||
|
||||
const body = await req.json() as {
|
||||
query: string;
|
||||
}
|
||||
|
||||
const resp = await fetch(`https://cf-ai-backend.dhravya.workers.dev/ask`, {
|
||||
headers: {
|
||||
"X-Custom-Auth-Key": env.BACKEND_SECURITY_KEY,
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
query: body.query,
|
||||
}),
|
||||
})
|
||||
|
||||
if (resp.status !== 200 || !resp.ok) {
|
||||
const errorData = await resp.json();
|
||||
return new Response(JSON.stringify({ message: "Error in CF function", error: errorData }), { status: resp.status });
|
||||
}
|
||||
|
||||
// Stream the response back to the client
|
||||
const { readable, writable } = new TransformStream();
|
||||
resp && resp.body!.pipeTo(writable);
|
||||
|
||||
return new Response(readable, { status: 200 });
|
||||
}
|
||||
134
apps/web/src/components/QueryAI.tsx
Normal file
134
apps/web/src/components/QueryAI.tsx
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
'use client';
|
||||
|
||||
import { Label } from './ui/label';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Input } from './ui/input';
|
||||
import { Button } from './ui/button';
|
||||
import SearchResults from './SearchResults';
|
||||
|
||||
function QueryAI() {
|
||||
const [searchResults, setSearchResults] = useState<string[]>([]);
|
||||
const [isAiLoading, setIsAiLoading] = useState(false);
|
||||
|
||||
const [aiResponse, setAIResponse] = useState('');
|
||||
const [input, setInput] = useState('');
|
||||
const [toBeParsed, setToBeParsed] = useState('');
|
||||
|
||||
const handleStreamData = (newChunk: string) => {
|
||||
// Append the new chunk to the existing data to be parsed
|
||||
setToBeParsed((prev) => prev + newChunk);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Define a function to try parsing the accumulated data
|
||||
const tryParseAccumulatedData = () => {
|
||||
// Attempt to parse the "toBeParsed" state as JSON
|
||||
try {
|
||||
// Split the accumulated data by the known delimiter "\n\n"
|
||||
const parts = toBeParsed.split('\n\n');
|
||||
let remainingData = '';
|
||||
|
||||
// Process each part to extract JSON objects
|
||||
parts.forEach((part, index) => {
|
||||
try {
|
||||
const parsedPart = JSON.parse(part.replace('data: ', '')); // Try to parse the part as JSON
|
||||
|
||||
// If the part is the last one and couldn't be parsed, keep it to accumulate more data
|
||||
if (index === parts.length - 1 && !parsedPart) {
|
||||
remainingData = part;
|
||||
} else if (parsedPart && parsedPart.response) {
|
||||
// If the part is parsable and has the "response" field, update the AI response state
|
||||
setAIResponse((prev) => prev + parsedPart.response);
|
||||
}
|
||||
} catch (error) {
|
||||
// If parsing fails and it's not the last part, it's a malformed JSON
|
||||
if (index !== parts.length - 1) {
|
||||
console.error('Malformed JSON part: ', part);
|
||||
} else {
|
||||
// If it's the last part, it may be incomplete, so keep it
|
||||
remainingData = part;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Update the toBeParsed state to only contain the unparsed remainder
|
||||
if (remainingData !== toBeParsed) {
|
||||
setToBeParsed(remainingData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing accumulated data: ', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Call the parsing function if there's data to be parsed
|
||||
if (toBeParsed) {
|
||||
tryParseAccumulatedData();
|
||||
}
|
||||
}, [toBeParsed]);
|
||||
|
||||
const getSearchResults = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setIsAiLoading(true);
|
||||
|
||||
const sourcesResponse = await fetch(
|
||||
`/api/query?sourcesOnly=true&q=${input}`,
|
||||
);
|
||||
|
||||
const sourcesInJson = (await sourcesResponse.json()) as {
|
||||
ids: string[];
|
||||
};
|
||||
|
||||
setSearchResults(sourcesInJson.ids);
|
||||
|
||||
const response = await fetch(`/api/query?q=${input}`);
|
||||
|
||||
if (response.body) {
|
||||
let reader = response.body.getReader();
|
||||
let decoder = new TextDecoder('utf-8');
|
||||
let result = '';
|
||||
|
||||
// @ts-ignore
|
||||
reader.read().then(function processText({ done, value }) {
|
||||
if (done) {
|
||||
// setSearchResults(JSON.parse(result.replace('data: ', '')));
|
||||
// setIsAiLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
handleStreamData(decoder.decode(value));
|
||||
|
||||
return reader.read().then(processText);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-2xl mx-auto">
|
||||
<form onSubmit={async (e) => await getSearchResults(e)} className="mt-8">
|
||||
<Label htmlFor="searchInput">Ask your SuperMemory</Label>
|
||||
<div className="flex flex-col md:flex-row md:w-full md:items-center space-y-2 md:space-y-0 md:space-x-2">
|
||||
<Input
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
placeholder="Search using AI... ✨"
|
||||
id="searchInput"
|
||||
/>
|
||||
<Button
|
||||
disabled={isAiLoading}
|
||||
className="max-w-min md:w-full"
|
||||
type="submit"
|
||||
variant="default"
|
||||
>
|
||||
Ask AI
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{searchResults && (
|
||||
<SearchResults aiResponse={aiResponse} sources={searchResults} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default QueryAI;
|
||||
38
apps/web/src/components/SearchResults.tsx
Normal file
38
apps/web/src/components/SearchResults.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
'use client'
|
||||
|
||||
import React from 'react';
|
||||
import { Card, CardContent } from './ui/card';
|
||||
import Markdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm'
|
||||
|
||||
function SearchResults({
|
||||
aiResponse,
|
||||
sources,
|
||||
}: {
|
||||
aiResponse: string;
|
||||
sources: string[];
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
backgroundImage: `linear-gradient(to right, #E5D9F2, #CDC1FF)`,
|
||||
}}
|
||||
className="w-full max-w-2xl mx-auto px-4 py-6 space-y-6 border mt-4 rounded-xl"
|
||||
>
|
||||
<div className="text-start">
|
||||
<div className="text-xl text-black">
|
||||
<Markdown remarkPlugins={[remarkGfm]}>{aiResponse.replace('</s>', '')}</Markdown>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-6">
|
||||
{sources.map((value, index) => (
|
||||
<Card key={index}>
|
||||
<CardContent className="space-y-2">{value}</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SearchResults;
|
||||
200
apps/web/src/components/ui/dropdown-menu.tsx
Normal file
200
apps/web/src/components/ui/dropdown-menu.tsx
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
"use client"
|
||||
|
||||
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"
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root
|
||||
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
||||
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
||||
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
||||
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
||||
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef<
|
||||
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-gray-100 data-[state=open]:bg-gray-100 dark:focus:bg-gray-800 dark:data-[state=open]:bg-gray-800",
|
||||
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>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-gray-200 bg-white p-1 text-gray-950 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 dark:border-gray-800 dark:bg-gray-950 dark:text-gray-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSubContent.displayName =
|
||||
DropdownMenuPrimitive.SubContent.displayName
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
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 border-gray-200 bg-white p-1 text-gray-950 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 dark:border-gray-800 dark:bg-gray-950 dark:text-gray-50",
|
||||
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
|
||||
}
|
||||
>(({ 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-gray-100 focus:text-gray-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-gray-800 dark:focus:text-gray-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>
|
||||
>(({ 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-gray-100 focus:text-gray-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-gray-800 dark:focus:text-gray-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>
|
||||
>(({ 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-gray-100 focus:text-gray-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-gray-800 dark:focus:text-gray-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
|
||||
}
|
||||
>(({ 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
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef<
|
||||
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-gray-100 dark:bg-gray-800", 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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
}
|
||||
26
apps/web/src/components/ui/label.tsx
Normal file
26
apps/web/src/components/ui/label.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
"use client"
|
||||
|
||||
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"
|
||||
|
||||
const labelVariants = cva(
|
||||
"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>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
|
||||
export { Label }
|
||||
Loading…
Add table
Reference in a new issue