+
-
+
setShareDialogOpen(false)}
+ />
@@ -189,6 +95,7 @@ export function SidebarActions({
disabled={isRemovePending}
onClick={event => {
event.preventDefault()
+ // @ts-ignore
startRemoveTransition(async () => {
const result = await removeChat({
id: chat.id,
diff --git a/components/sidebar-desktop.tsx b/components/sidebar-desktop.tsx
new file mode 100644
index 000000000..7bc0e19c1
--- /dev/null
+++ b/components/sidebar-desktop.tsx
@@ -0,0 +1,19 @@
+import { Sidebar } from '@/components/sidebar'
+
+import { auth } from '@/auth'
+import { ChatHistory } from '@/components/chat-history'
+
+export async function SidebarDesktop() {
+ const session = await auth()
+
+ if (!session?.user?.id) {
+ return null
+ }
+
+ return (
+
+ {/* @ts-ignore */}
+
+
+ )
+}
diff --git a/components/sidebar-item.tsx b/components/sidebar-item.tsx
index a58743cc4..a4e40443b 100644
--- a/components/sidebar-item.tsx
+++ b/components/sidebar-item.tsx
@@ -1,10 +1,12 @@
'use client'
+import * as React from 'react'
+
import Link from 'next/link'
import { usePathname } from 'next/navigation'
-import { type Chat } from '@/lib/types'
-import { cn } from '@/lib/utils'
+import { motion } from 'framer-motion'
+
import { buttonVariants } from '@/components/ui/button'
import { IconMessage, IconUsers } from '@/components/ui/icons'
import {
@@ -12,20 +14,45 @@ import {
TooltipContent,
TooltipTrigger
} from '@/components/ui/tooltip'
+import { useLocalStorage } from '@/lib/hooks/use-local-storage'
+import { type Chat } from '@/lib/types'
+import { cn } from '@/lib/utils'
interface SidebarItemProps {
+ index: number
chat: Chat
children: React.ReactNode
}
-export function SidebarItem({ chat, children }: SidebarItemProps) {
+export function SidebarItem({ index, chat, children }: SidebarItemProps) {
const pathname = usePathname()
+
const isActive = pathname === chat.path
+ const [newChatId, setNewChatId] = useLocalStorage('newChatId', null)
+ const shouldAnimate = index === 0 && isActive && newChatId
if (!chat?.id) return null
return (
-
+
{chat.sharePath ? (
@@ -45,18 +72,53 @@ export function SidebarItem({ chat, children }: SidebarItemProps) {
href={chat.path}
className={cn(
buttonVariants({ variant: 'ghost' }),
- 'group w-full pl-8 pr-16',
- isActive && 'bg-accent'
+ 'group w-full px-8 transition-colors hover:bg-zinc-200/40 dark:hover:bg-zinc-300/10',
+ isActive && 'bg-zinc-200 pr-16 font-semibold dark:bg-zinc-800'
)}
>
- {chat.title}
+
+ {shouldAnimate ? (
+ chat.title.split('').map((character, index) => (
+ {
+ if (index === chat.title.length - 1) {
+ setNewChatId(null)
+ }
+ }}
+ >
+ {character}
+
+ ))
+ ) : (
+ {chat.title}
+ )}
+
{isActive && {children}
}
-
+
)
}
diff --git a/components/sidebar-items.tsx b/components/sidebar-items.tsx
new file mode 100644
index 000000000..11cc7fc47
--- /dev/null
+++ b/components/sidebar-items.tsx
@@ -0,0 +1,42 @@
+'use client'
+
+import { Chat } from '@/lib/types'
+import { AnimatePresence, motion } from 'framer-motion'
+
+import { removeChat, shareChat } from '@/app/actions'
+
+import { SidebarActions } from '@/components/sidebar-actions'
+import { SidebarItem } from '@/components/sidebar-item'
+
+interface SidebarItemsProps {
+ chats?: Chat[]
+}
+
+export function SidebarItems({ chats }: SidebarItemsProps) {
+ if (!chats?.length) return null
+
+ return (
+
+ {chats.map(
+ (chat, index) =>
+ chat && (
+
+
+
+
+
+ )
+ )}
+
+ )
+}
diff --git a/components/sidebar-list.tsx b/components/sidebar-list.tsx
index de2049abf..0a7fb1ade 100644
--- a/components/sidebar-list.tsx
+++ b/components/sidebar-list.tsx
@@ -1,36 +1,38 @@
-import { getChats, removeChat, shareChat } from '@/app/actions'
-import { SidebarActions } from '@/components/sidebar-actions'
-import { SidebarItem } from '@/components/sidebar-item'
+import { clearChats, getChats } from '@/app/actions'
+import { ClearHistory } from '@/components/clear-history'
+import { SidebarItems } from '@/components/sidebar-items'
+import { ThemeToggle } from '@/components/theme-toggle'
+import { cache } from 'react'
-export interface SidebarListProps {
+interface SidebarListProps {
userId?: string
+ children?: React.ReactNode
}
+const loadChats = cache(async (userId?: string) => {
+ return await getChats(userId)
+})
+
export async function SidebarList({ userId }: SidebarListProps) {
- const chats = await getChats(userId)
+ const chats = await loadChats(userId)
return (
-
- {chats?.length ? (
-
- {chats.map(
- chat =>
- chat && (
-
-
-
- )
- )}
-
- ) : (
-
- )}
+
+
+ {chats?.length ? (
+
+
+
+ ) : (
+
+ )}
+
+
+
+ 0} />
+
)
}
diff --git a/components/sidebar-mobile.tsx b/components/sidebar-mobile.tsx
new file mode 100644
index 000000000..50adcb17b
--- /dev/null
+++ b/components/sidebar-mobile.tsx
@@ -0,0 +1,28 @@
+'use client'
+
+import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet'
+
+import { Sidebar } from '@/components/sidebar'
+import { Button } from '@/components/ui/button'
+
+import { IconSidebar } from '@/components/ui/icons'
+
+interface SidebarMobileProps {
+ children: React.ReactNode
+}
+
+export function SidebarMobile({ children }: SidebarMobileProps) {
+ return (
+
+
+
+
+ Toggle Sidebar
+
+
+
+ {children}
+
+
+ )
+}
diff --git a/components/sidebar-toggle.tsx b/components/sidebar-toggle.tsx
new file mode 100644
index 000000000..951d60657
--- /dev/null
+++ b/components/sidebar-toggle.tsx
@@ -0,0 +1,24 @@
+'use client'
+
+import * as React from 'react'
+
+import { useSidebar } from '@/lib/hooks/use-sidebar'
+import { Button } from '@/components/ui/button'
+import { IconSidebar } from '@/components/ui/icons'
+
+export function SidebarToggle() {
+ const { toggleSidebar } = useSidebar()
+
+ return (
+
{
+ toggleSidebar()
+ }}
+ >
+
+ Toggle Sidebar
+
+ )
+}
diff --git a/components/sidebar.tsx b/components/sidebar.tsx
index e842b12e4..52dc5bd76 100644
--- a/components/sidebar.tsx
+++ b/components/sidebar.tsx
@@ -2,35 +2,20 @@
import * as React from 'react'
-import { Button } from '@/components/ui/button'
-import {
- Sheet,
- SheetContent,
- SheetHeader,
- SheetTitle,
- SheetTrigger
-} from '@/components/ui/sheet'
-import { IconSidebar } from '@/components/ui/icons'
+import { useSidebar } from '@/lib/hooks/use-sidebar'
+import { cn } from '@/lib/utils'
-export interface SidebarProps {
- children?: React.ReactNode
-}
+export interface SidebarProps extends React.ComponentProps<'div'> {}
+
+export function Sidebar({ className, children }: SidebarProps) {
+ const { isSidebarOpen, isLoading } = useSidebar()
-export function Sidebar({ children }: SidebarProps) {
return (
-
-
-
-
- Toggle Sidebar
-
-
-
-
- Chat History
-
- {children}
-
-
+
+ {children}
+
)
}
diff --git a/lib/hooks/use-sidebar.tsx b/lib/hooks/use-sidebar.tsx
new file mode 100644
index 000000000..4a52baf19
--- /dev/null
+++ b/lib/hooks/use-sidebar.tsx
@@ -0,0 +1,60 @@
+'use client'
+
+import * as React from 'react'
+
+const LOCAL_STORAGE_KEY = 'sidebar'
+
+interface SidebarContext {
+ isSidebarOpen: boolean
+ toggleSidebar: () => void
+ isLoading: boolean
+}
+
+const SidebarContext = React.createContext
(
+ undefined
+)
+
+export function useSidebar() {
+ const context = React.useContext(SidebarContext)
+ if (!context) {
+ throw new Error('useSidebarContext must be used within a SidebarProvider')
+ }
+ return context
+}
+
+interface SidebarProviderProps {
+ children: React.ReactNode
+}
+
+export function SidebarProvider({ children }: SidebarProviderProps) {
+ const [isSidebarOpen, setSidebarOpen] = React.useState(true)
+ const [isLoading, setLoading] = React.useState(true)
+
+ React.useEffect(() => {
+ const value = localStorage.getItem(LOCAL_STORAGE_KEY)
+ if (value) {
+ setSidebarOpen(JSON.parse(value))
+ }
+ setLoading(false)
+ }, [])
+
+ const toggleSidebar = () => {
+ setSidebarOpen(value => {
+ const newState = !value
+ localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(newState))
+ return newState
+ })
+ }
+
+ if (isLoading) {
+ return null
+ }
+
+ return (
+
+ {children}
+
+ )
+}
diff --git a/package.json b/package.json
index bd1f9be9c..2166b7dec 100644
--- a/package.json
+++ b/package.json
@@ -28,6 +28,7 @@
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"focus-trap-react": "^10.2.3",
+ "framer-motion": "^10.16.12",
"geist": "^1.1.0",
"nanoid": "^5.0.3",
"next": "14.0.4-canary.17",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 136bd8623..a41c2e9c9 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -53,6 +53,9 @@ dependencies:
focus-trap-react:
specifier: ^10.2.3
version: 10.2.3(prop-types@15.8.1)(react-dom@18.2.0)(react@18.2.0)
+ framer-motion:
+ specifier: ^10.16.12
+ version: 10.16.12(react-dom@18.2.0)(react@18.2.0)
geist:
specifier: ^1.1.0
version: 1.1.0(next@14.0.4-canary.17)
@@ -221,6 +224,19 @@ packages:
to-fast-properties: 2.0.0
dev: false
+ /@emotion/is-prop-valid@0.8.8:
+ resolution: {integrity: sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==}
+ requiresBuild: true
+ dependencies:
+ '@emotion/memoize': 0.7.4
+ dev: false
+ optional: true
+
+ /@emotion/memoize@0.7.4:
+ resolution: {integrity: sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==}
+ dev: false
+ optional: true
+
/@eslint-community/eslint-utils@4.4.0(eslint@8.54.0):
resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -2618,6 +2634,24 @@ packages:
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
dev: true
+ /framer-motion@10.16.12(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-w7Yzx0OzQ5Uh6uNkxaX+4TuAPuOKz3haSbjmHpdrqDpGuCJCpq6YP9Dy7JJWdZ6mJjndrg3Ao3vUwDajKNikCA==}
+ peerDependencies:
+ react: ^18.0.0
+ react-dom: ^18.0.0
+ peerDependenciesMeta:
+ react:
+ optional: true
+ react-dom:
+ optional: true
+ dependencies:
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ tslib: 2.6.2
+ optionalDependencies:
+ '@emotion/is-prop-valid': 0.8.8
+ dev: false
+
/fs.realpath@1.0.0:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
dev: true