feat: refactor chat sharing implementation
This commit is contained in:
parent
8cc3fea048
commit
137bdadaf9
12 changed files with 262 additions and 124 deletions
|
|
@ -2,10 +2,9 @@
|
||||||
|
|
||||||
import { revalidatePath } from 'next/cache'
|
import { revalidatePath } from 'next/cache'
|
||||||
import { kv } from '@vercel/kv'
|
import { kv } from '@vercel/kv'
|
||||||
|
import { currentUser } from '@clerk/nextjs'
|
||||||
|
|
||||||
import { type Chat } from '@/lib/types'
|
import { type Chat } from '@/lib/types'
|
||||||
import { currentUser } from '@clerk/nextjs'
|
|
||||||
import { nanoid } from '@/lib/utils'
|
|
||||||
|
|
||||||
export async function getChats(userId?: string | null) {
|
export async function getChats(userId?: string | null) {
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
|
|
@ -52,9 +51,11 @@ export async function removeChat({ id, path }: { id: string; path: string }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const uid = await kv.hget<string>(`chat:${id}`, 'userId')
|
const uid = await kv.hget<string>(`chat:${id}`, 'userId')
|
||||||
|
|
||||||
if (uid !== user.id) {
|
if (uid !== user.id) {
|
||||||
throw new Error('Unauthorized')
|
throw new Error('Unauthorized')
|
||||||
}
|
}
|
||||||
|
|
||||||
await kv.del(`chat:${id}`)
|
await kv.del(`chat:${id}`)
|
||||||
await kv.zrem(`user:chat:${user.id}`, `chat:${id}`)
|
await kv.zrem(`user:chat:${user.id}`, `chat:${id}`)
|
||||||
|
|
||||||
|
|
@ -85,6 +86,16 @@ export async function clearChats() {
|
||||||
revalidatePath('/')
|
revalidatePath('/')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getSharedChat(id: string) {
|
||||||
|
const chat = await kv.hgetall<Chat>(`chat:${id}`)
|
||||||
|
|
||||||
|
if (!chat || !chat.sharePath) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return chat
|
||||||
|
}
|
||||||
|
|
||||||
export async function shareChat(chat: Chat) {
|
export async function shareChat(chat: Chat) {
|
||||||
const user = await currentUser()
|
const user = await currentUser()
|
||||||
|
|
||||||
|
|
@ -92,23 +103,12 @@ export async function shareChat(chat: Chat) {
|
||||||
throw new Error('Unauthorized')
|
throw new Error('Unauthorized')
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = nanoid()
|
|
||||||
const createdAt = Date.now()
|
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
id,
|
...chat,
|
||||||
title: chat.title,
|
sharePath: `/share/${chat.id}`
|
||||||
userId: user.id,
|
|
||||||
createdAt,
|
|
||||||
path: `/share/${id}`,
|
|
||||||
chat
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await kv.hmset(`share:${id}`, payload)
|
await kv.hmset(`chat:${chat.id}`, payload)
|
||||||
await kv.zadd(`user:share:${user.id}`, {
|
|
||||||
score: createdAt,
|
|
||||||
member: `share:${id}`
|
|
||||||
})
|
|
||||||
|
|
||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import '@/app/globals.css'
|
||||||
import { fontMono, fontSans } from '@/lib/fonts'
|
import { fontMono, fontSans } from '@/lib/fonts'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { TailwindIndicator } from '@/components/tailwind-indicator'
|
import { TailwindIndicator } from '@/components/tailwind-indicator'
|
||||||
import { ThemeProvider } from '@/components/theme-provider'
|
import { Providers } from '@/components/providers'
|
||||||
import { Header } from '@/components/header'
|
import { Header } from '@/components/header'
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
|
|
@ -44,14 +44,14 @@ export default function RootLayout({ children }: RootLayoutProps) {
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Toaster />
|
<Toaster />
|
||||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
<Providers attribute="class" defaultTheme="system" enableSystem>
|
||||||
<div className="flex min-h-screen flex-col">
|
<div className="flex min-h-screen flex-col">
|
||||||
{/* @ts-ignore */}
|
{/* @ts-ignore */}
|
||||||
<Header />
|
<Header />
|
||||||
<main className="flex-1 bg-muted/50">{children}</main>
|
<main className="flex-1 bg-muted/50">{children}</main>
|
||||||
</div>
|
</div>
|
||||||
<TailwindIndicator />
|
<TailwindIndicator />
|
||||||
</ThemeProvider>
|
</Providers>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
</ClerkProvider>
|
</ClerkProvider>
|
||||||
|
|
|
||||||
35
app/share/[id]/page.tsx
Normal file
35
app/share/[id]/page.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { type Metadata } from 'next'
|
||||||
|
import { auth } from '@clerk/nextjs'
|
||||||
|
|
||||||
|
import { Chat } from '@/components/chat'
|
||||||
|
import { getChat, getSharedChat } from '@/app/actions'
|
||||||
|
import { notFound } from 'next/navigation'
|
||||||
|
|
||||||
|
// export const runtime = 'edge'
|
||||||
|
export const preferredRegion = 'home'
|
||||||
|
|
||||||
|
export interface SharePageProps {
|
||||||
|
params: {
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateMetadata({
|
||||||
|
params
|
||||||
|
}: SharePageProps): Promise<Metadata> {
|
||||||
|
const { user } = await auth()
|
||||||
|
const chat = await getChat(params.id, user?.id ?? '')
|
||||||
|
return {
|
||||||
|
title: chat?.title.slice(0, 50) ?? 'Chat'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function SharePage({ params }: SharePageProps) {
|
||||||
|
const chat = await getSharedChat(params.id)
|
||||||
|
|
||||||
|
if (!chat || !chat?.sharePath) {
|
||||||
|
notFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Chat id={chat.id} initialMessages={chat.messages} />
|
||||||
|
}
|
||||||
|
|
@ -9,7 +9,6 @@ import { Button, buttonVariants } from '@/components/ui/button'
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipProvider,
|
|
||||||
TooltipTrigger
|
TooltipTrigger
|
||||||
} from '@/components/ui/tooltip'
|
} from '@/components/ui/tooltip'
|
||||||
import { IconArrowElbow, IconPlus } from '@/components/ui/icons'
|
import { IconArrowElbow, IconPlus } from '@/components/ui/icons'
|
||||||
|
|
@ -47,7 +46,6 @@ export function PromptForm({
|
||||||
}}
|
}}
|
||||||
ref={formRef}
|
ref={formRef}
|
||||||
>
|
>
|
||||||
<TooltipProvider>
|
|
||||||
<div className="relative flex w-full grow flex-col overflow-hidden bg-background px-8 sm:rounded-md sm:border sm:px-12">
|
<div className="relative flex w-full grow flex-col overflow-hidden bg-background px-8 sm:rounded-md sm:border sm:px-12">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
|
|
@ -91,7 +89,6 @@ export function PromptForm({
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TooltipProvider>
|
|
||||||
</form>
|
</form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
14
components/providers.tsx
Normal file
14
components/providers.tsx
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import * as React from 'react'
|
||||||
|
import { ThemeProvider as NextThemesProvider } from 'next-themes'
|
||||||
|
import { ThemeProviderProps } from 'next-themes/dist/types'
|
||||||
|
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||||
|
|
||||||
|
export function Providers({ children, ...props }: ThemeProviderProps) {
|
||||||
|
return (
|
||||||
|
<NextThemesProvider {...props}>
|
||||||
|
<TooltipProvider>{children}</TooltipProvider>
|
||||||
|
</NextThemesProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -4,8 +4,8 @@ import * as React from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import { toast } from 'react-hot-toast'
|
import { toast } from 'react-hot-toast'
|
||||||
|
|
||||||
import { Share, type Chat, ServerActionResult } from '@/lib/types'
|
import { type Chat, ServerActionResult } from '@/lib/types'
|
||||||
import { formatDate } from '@/lib/utils'
|
import { cn, formatDate } from '@/lib/utils'
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
|
|
@ -25,12 +25,24 @@ import {
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle
|
DialogTitle
|
||||||
} from '@/components/ui/dialog'
|
} from '@/components/ui/dialog'
|
||||||
import { IconShare, IconSpinner, IconTrash } from '@/components/ui/icons'
|
import {
|
||||||
|
IconShare,
|
||||||
|
IconSpinner,
|
||||||
|
IconTrash,
|
||||||
|
IconUsers
|
||||||
|
} from '@/components/ui/icons'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { badgeVariants } from '@/components/ui/badge'
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger
|
||||||
|
} from '@/components/ui/tooltip'
|
||||||
|
|
||||||
interface SidebarActionsProps {
|
interface SidebarActionsProps {
|
||||||
chat: Chat
|
chat: Chat
|
||||||
removeChat: (args: { id: string; path: string }) => Promise<void>
|
removeChat: (args: { id: string; path: string }) => Promise<void>
|
||||||
shareChat: (chat: Chat) => ServerActionResult<Share>
|
shareChat: (chat: Chat) => ServerActionResult<Chat>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SidebarActions({
|
export function SidebarActions({
|
||||||
|
|
@ -44,59 +56,13 @@ export function SidebarActions({
|
||||||
const [isSharePending, startShareTransition] = React.useTransition()
|
const [isSharePending, startShareTransition] = React.useTransition()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
return (
|
const copyShareLink = React.useCallback(async (chat: Chat) => {
|
||||||
<>
|
if (!chat.sharePath) {
|
||||||
<div className="space-x-1">
|
return toast.error('Could not copy share link to clipboard')
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="h-6 w-6 p-0 hover:bg-background"
|
|
||||||
onClick={() => setShareDialogOpen(true)}
|
|
||||||
>
|
|
||||||
<IconShare />
|
|
||||||
<span className="sr-only">Share</span>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="h-6 w-6 p-0 hover:bg-background"
|
|
||||||
disabled={isRemovePending}
|
|
||||||
onClick={() => setDeleteDialogOpen(true)}
|
|
||||||
>
|
|
||||||
<IconTrash />
|
|
||||||
<span className="sr-only">Delete</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Dialog open={shareDialogOpen} onOpenChange={setShareDialogOpen}>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Share link to chat</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Messages you send after creating your link won't be shared.
|
|
||||||
Anyone with the URL will be able to view the shared chat.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="space-y-1 rounded-md border p-4 text-sm">
|
|
||||||
<div className="font-medium">{chat.title}</div>
|
|
||||||
<div className="text-muted-foreground">
|
|
||||||
{formatDate(chat.createdAt)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<DialogFooter className="items-center">
|
|
||||||
<p className="mr-auto text-sm text-muted-foreground">
|
|
||||||
Any link you have shared before will be deleted.
|
|
||||||
</p>
|
|
||||||
<Button
|
|
||||||
disabled={isSharePending}
|
|
||||||
onClick={() => {
|
|
||||||
startShareTransition(async () => {
|
|
||||||
const result = await shareChat(chat)
|
|
||||||
|
|
||||||
if (!('id' in result)) {
|
|
||||||
toast.error(result.message)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = new URL(window.location.href)
|
const url = new URL(window.location.href)
|
||||||
url.pathname = result.path
|
url.pathname = chat.sharePath
|
||||||
navigator.clipboard.writeText(url.toString())
|
navigator.clipboard.writeText(url.toString())
|
||||||
setShareDialogOpen(false)
|
setShareDialogOpen(false)
|
||||||
toast.success('Share link copied to clipboard', {
|
toast.success('Share link copied to clipboard', {
|
||||||
|
|
@ -111,6 +77,85 @@ export function SidebarActions({
|
||||||
secondary: 'black'
|
secondary: 'black'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="space-x-1">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="h-6 w-6 p-0 hover:bg-background"
|
||||||
|
onClick={() => setShareDialogOpen(true)}
|
||||||
|
>
|
||||||
|
<IconShare />
|
||||||
|
<span className="sr-only">Share</span>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Share chat</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="h-6 w-6 p-0 hover:bg-background"
|
||||||
|
disabled={isRemovePending}
|
||||||
|
onClick={() => setDeleteDialogOpen(true)}
|
||||||
|
>
|
||||||
|
<IconTrash />
|
||||||
|
<span className="sr-only">Delete</span>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Delete chat</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<Dialog open={shareDialogOpen} onOpenChange={setShareDialogOpen}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Share link to chat</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Anyone with the URL will be able to view the shared chat.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-1 rounded-md border p-4 text-sm">
|
||||||
|
<div className="font-medium">{chat.title}</div>
|
||||||
|
<div className="text-muted-foreground">
|
||||||
|
{formatDate(chat.createdAt)} · {chat.messages.length} messages
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter className="items-center">
|
||||||
|
{chat.sharePath && (
|
||||||
|
<Link
|
||||||
|
href={chat.sharePath}
|
||||||
|
className={cn(
|
||||||
|
badgeVariants({ variant: 'secondary' }),
|
||||||
|
'mr-auto'
|
||||||
|
)}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<IconUsers className="mr-2" />
|
||||||
|
{chat.sharePath}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
disabled={isSharePending}
|
||||||
|
onClick={() => {
|
||||||
|
startShareTransition(async () => {
|
||||||
|
if (chat.sharePath) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500))
|
||||||
|
copyShareLink(chat)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await shareChat(chat)
|
||||||
|
|
||||||
|
if (!('id' in result)) {
|
||||||
|
toast.error(result.message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
copyShareLink(result)
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,12 @@ import { usePathname } from 'next/navigation'
|
||||||
import { type Chat } from '@/lib/types'
|
import { type Chat } from '@/lib/types'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { buttonVariants } from '@/components/ui/button'
|
import { buttonVariants } from '@/components/ui/button'
|
||||||
import { IconMessage } from '@/components/ui/icons'
|
import { IconMessage, IconUsers } from '@/components/ui/icons'
|
||||||
import { SidebarActions } from '@/components/sidebar-actions'
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger
|
||||||
|
} from '@/components/ui/tooltip'
|
||||||
|
|
||||||
interface SidebarItemProps {
|
interface SidebarItemProps {
|
||||||
chat: Chat
|
chat: Chat
|
||||||
|
|
@ -30,7 +34,16 @@ export function SidebarItem({ chat, children }: SidebarItemProps) {
|
||||||
isActive && 'bg-accent'
|
isActive && 'bg-accent'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
{chat.sharePath ? (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<IconUsers className="mr-2" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>This is a shared chat.</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
<IconMessage className="mr-2" />
|
<IconMessage className="mr-2" />
|
||||||
|
)}
|
||||||
<div
|
<div
|
||||||
className="relative max-h-5 flex-1 select-none overflow-hidden text-ellipsis break-all"
|
className="relative max-h-5 flex-1 select-none overflow-hidden text-ellipsis break-all"
|
||||||
title={chat.title}
|
title={chat.title}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
'use client'
|
|
||||||
|
|
||||||
import * as React from 'react'
|
|
||||||
import { ThemeProvider as NextThemesProvider } from 'next-themes'
|
|
||||||
import { ThemeProviderProps } from 'next-themes/dist/types'
|
|
||||||
|
|
||||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
|
||||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
|
||||||
}
|
|
||||||
36
components/ui/badge.tsx
Normal file
36
components/ui/badge.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const badgeVariants = cva(
|
||||||
|
"inline-flex items-center border rounded-full px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
"bg-primary hover:bg-primary/80 border-transparent text-primary-foreground",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary hover:bg-secondary/80 border-transparent text-secondary-foreground",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive hover:bg-destructive/80 border-transparent text-destructive-foreground",
|
||||||
|
outline: "text-foreground",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export interface BadgeProps
|
||||||
|
extends React.HTMLAttributes<HTMLDivElement>,
|
||||||
|
VariantProps<typeof badgeVariants> {}
|
||||||
|
|
||||||
|
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||||
|
return (
|
||||||
|
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Badge, badgeVariants }
|
||||||
|
|
@ -428,6 +428,20 @@ function IconShare({ className, ...props }: React.ComponentProps<'svg'>) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function IconUsers({ className, ...props }: React.ComponentProps<'svg'>) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="currentColor"
|
||||||
|
className={cn('h-4 w-4', className)}
|
||||||
|
viewBox="0 0 256 256"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path d="M117.25 157.92a60 60 0 1 0-66.5 0 95.83 95.83 0 0 0-47.22 37.71 8 8 0 1 0 13.4 8.74 80 80 0 0 1 134.14 0 8 8 0 0 0 13.4-8.74 95.83 95.83 0 0 0-47.22-37.71ZM40 108a44 44 0 1 1 44 44 44.05 44.05 0 0 1-44-44Zm210.14 98.7a8 8 0 0 1-11.07-2.33A79.83 79.83 0 0 0 172 168a8 8 0 0 1 0-16 44 44 0 1 0-16.34-84.87 8 8 0 1 1-5.94-14.85 60 60 0 0 1 55.53 105.64 95.83 95.83 0 0 1 47.22 37.71 8 8 0 0 1-2.33 11.07Z" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
IconEdit,
|
IconEdit,
|
||||||
IconNextChat,
|
IconNextChat,
|
||||||
|
|
@ -452,5 +466,6 @@ export {
|
||||||
IconCheck,
|
IconCheck,
|
||||||
IconDownload,
|
IconDownload,
|
||||||
IconClose,
|
IconClose,
|
||||||
IconShare
|
IconShare,
|
||||||
|
IconUsers
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ const TooltipContent = React.forwardRef<
|
||||||
ref={ref}
|
ref={ref}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
'z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm font-medium text-popover-foreground shadow-md animate-in fade-in-50 data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1',
|
'z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-xs font-medium text-popover-foreground shadow-md animate-in fade-in-50 data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
||||||
10
lib/types.ts
10
lib/types.ts
|
|
@ -7,15 +7,7 @@ export interface Chat extends Record<string, any> {
|
||||||
userId: string
|
userId: string
|
||||||
path: string
|
path: string
|
||||||
messages: Message[]
|
messages: Message[]
|
||||||
}
|
sharePath?: string
|
||||||
|
|
||||||
export interface Share {
|
|
||||||
id: string
|
|
||||||
title: string
|
|
||||||
createdAt: number
|
|
||||||
userId: string
|
|
||||||
path: string
|
|
||||||
chat: Chat
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ServerActionResult<Result> = Promise<Result | Error>
|
export type ServerActionResult<Result> = Promise<Result | Error>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue