Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,8 @@ CREATE TABLE chats (
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
public BOOLEAN DEFAULT FALSE NOT NULL,
pinned BOOLEAN DEFAULT FALSE NOT NULL,
pinned_at TIMESTAMPTZ NULL,
CONSTRAINT chats_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
CONSTRAINT chats_project_id_fkey FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
);
Expand Down
44 changes: 44 additions & 0 deletions app/api/toggle-chat-pin/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { createClient } from "@/lib/supabase/server"
import { NextResponse } from "next/server"

export async function POST(request: Request) {
try {
const supabase = await createClient()
const { chatId, pinned } = await request.json()

if (!chatId || typeof pinned !== "boolean") {
return NextResponse.json(
{ error: "Missing chatId or pinned" },
{ status: 400 }
)
}

if (!supabase) {
return NextResponse.json({ success: true }, { status: 200 })
}

const toggle = pinned
? { pinned: true, pinned_at: new Date().toISOString() }
: { pinned: false, pinned_at: null }

const { error } = await supabase
.from("chats")
.update(toggle)
.eq("id", chatId)

if (error) {
return NextResponse.json(
{ error: "Failed to update pinned" },
{ status: 500 }
)
}

return NextResponse.json({ success: true }, { status: 200 })
} catch (error) {
console.error("toggle-chat-pin unhandled error:", error)
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
)
}
}
42 changes: 41 additions & 1 deletion app/components/history/command-history.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,14 @@ import {
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip"
import { useChats } from "@/lib/chat-store/chats/provider"
import { useChatSession } from "@/lib/chat-store/session/provider"
import type { Chats } from "@/lib/chat-store/types"
import { useChatPreview } from "@/lib/hooks/use-chat-preview"
import { useUserPreferences } from "@/lib/user-preference-store/provider"
import { cn } from "@/lib/utils"
import { Check, PencilSimple, TrashSimple, X } from "@phosphor-icons/react"
import { Pin, PinOff } from "lucide-react"
import { useRouter } from "next/navigation"
import { useCallback, useMemo, useRef, useState } from "react"
import { ChatPreviewPanel } from "./chat-preview-panel"
Expand Down Expand Up @@ -208,6 +210,7 @@ function CommandItemRow({
}: CommandItemRowProps) {
const { chatId } = useChatSession()
const isCurrentChat = chat.id === chatId
const { togglePinned } = useChats()

return (
<>
Expand All @@ -220,10 +223,33 @@ function CommandItemRow({

<div className="relative flex min-w-[140px] flex-shrink-0 items-center justify-end">
<div className="text-muted-foreground mr-2 text-xs transition-opacity duration-200 group-hover:opacity-0">
{formatDate(chat.created_at)}
{formatDate(chat.updated_at || chat.created_at)}
</div>

<div className="absolute right-0 flex translate-x-1 gap-1 opacity-0 transition-all duration-200 group-hover:translate-x-0 group-hover:opacity-100">
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon"
variant="ghost"
className="group/edit text-muted-foreground hover:bg-primary/10 size-8 transition-colors duration-150"
onClick={(e) => {
e.stopPropagation()
togglePinned(chat.id, !chat.pinned)
}}
disabled={!!editingId || !!deletingId}
aria-label={chat.pinned ? "Unpin" : "Pin"}
>
{chat.pinned ? (
<PinOff className="group-hover/edit:text-primary size-3 stroke-[1.5px] transition-colors duration-150" />
) : (
<Pin className="group-hover/edit:text-primary size-3 stroke-[1.5px] transition-colors duration-150" />
)}
</Button>
</TooltipTrigger>
<TooltipContent>{chat.pinned ? "Unpin" : "Pin"}</TooltipContent>
</Tooltip>

<Tooltip>
<TooltipTrigger asChild>
<Button
Expand Down Expand Up @@ -440,6 +466,8 @@ export function CommandHistory({
[chatHistory, searchQuery]
)

const { pinnedChats } = useChats()

const activePreviewChatId =
hoveredChatId || (isPreviewPanelHovered ? hoveredChatId : null)

Expand Down Expand Up @@ -570,6 +598,18 @@ export function CommandHistory({
<CommandEmpty>No chat history found.</CommandEmpty>
)}

{!searchQuery && pinnedChats.length > 0 && (
<CommandGroup
heading={
<div className="flex items-center gap-1 font-semibold break-all">
<Pin className="size-3" />
Pinned
</div>
}
>
{pinnedChats.map((chat) => renderChatItem(chat))}
</CommandGroup>
)}
{searchQuery ? (
<CommandGroup className="p-1.5">
{filteredChat.map((chat) => renderChatItem(chat))}
Expand Down
78 changes: 57 additions & 21 deletions app/components/history/drawer-history.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip"
import { useChats } from "@/lib/chat-store/chats/provider"
import { Chats } from "@/lib/chat-store/types"
import {
Check,
Expand All @@ -15,9 +16,10 @@ import {
TrashSimple,
X,
} from "@phosphor-icons/react"
import { Pin, PinOff } from "lucide-react"
import Link from "next/link"
import { useParams } from "next/navigation"
import { useCallback, useMemo, useState } from "react"
import React, { useCallback, useMemo, useState } from "react"
import { formatDate, groupChatsByDate } from "./utils"

type DrawerHistoryProps = {
Expand All @@ -37,21 +39,25 @@ export function DrawerHistory({
isOpen,
setIsOpen,
}: DrawerHistoryProps) {
const { pinnedChats, togglePinned } = useChats()
const [searchQuery, setSearchQuery] = useState("")
const [editingId, setEditingId] = useState<string | null>(null)
const [editTitle, setEditTitle] = useState("")
const [deletingId, setDeletingId] = useState<string | null>(null)
const params = useParams<{ chatId: string }>()

const handleOpenChange = useCallback((open: boolean) => {
setIsOpen(open)
if (!open) {
setSearchQuery("")
setEditingId(null)
setEditTitle("")
setDeletingId(null)
}
}, [setIsOpen])
const handleOpenChange = useCallback(
(open: boolean) => {
setIsOpen(open)
if (!open) {
setSearchQuery("")
setEditingId(null)
setEditTitle("")
setDeletingId(null)
}
},
[setIsOpen]
)

const handleEdit = useCallback((chat: Chats) => {
setEditingId(chat.id)
Expand Down Expand Up @@ -216,11 +222,28 @@ export function DrawerHistory({
{chat.title || "Untitled Chat"}
</span>
<span className="mr-2 text-xs font-normal text-gray-500">
{formatDate(chat?.created_at)}
{formatDate(chat?.updated_at || chat?.created_at)}
</span>
</Link>
<div className="flex items-center">
<div className="flex gap-1">
<Button
size="icon"
variant="ghost"
className="text-muted-foreground hover:text-foreground size-8"
onClick={(e) => {
e.preventDefault()
togglePinned(chat.id, !chat.pinned)
}}
type="button"
aria-label={chat.pinned ? "Unpin" : "Pin"}
>
{chat.pinned ? (
<PinOff className="size-4 stroke-[1.5px]" />
) : (
<Pin className="size-4 stroke-[1.5px]" />
)}
</Button>
<Button
size="icon"
variant="ghost"
Expand Down Expand Up @@ -264,6 +287,7 @@ export function DrawerHistory({
handleCancelDelete,
handleEdit,
handleDelete,
togglePinned,
]
)

Expand Down Expand Up @@ -301,17 +325,29 @@ export function DrawerHistory({
{filteredChat.map((chat) => renderChatItem(chat))}
</div>
) : (
// When not searching, display grouped by date
groupedChats?.map((group) => (
<div key={group.name} className="space-y-0.5">
<h3 className="text-muted-foreground pl-2 text-sm font-medium">
{group.name}
</h3>
<div className="space-y-2">
{group.chats.map((chat) => renderChatItem(chat))}
<>
{pinnedChats.length > 0 && (
<div className="space-y-0.5">
<h3 className="text-muted-foreground flex items-center gap-1 pl-2 text-sm font-medium">
<Pin className="size-3" />
Pinned
</h3>
<div className="space-y-2">
{pinnedChats.map((chat) => renderChatItem(chat))}
</div>
</div>
)}
{groupedChats?.map((group) => (
<div key={group.name} className="space-y-0.5">
<h3 className="text-muted-foreground pl-2 text-sm font-medium">
{group.name}
</h3>
<div className="space-y-2">
{group.chats.map((chat) => renderChatItem(chat))}
</div>
</div>
</div>
))
))}
</>
)}
</div>
</ScrollArea>
Expand Down
1 change: 1 addition & 0 deletions app/components/history/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export function groupChatsByDate(

chats.forEach((chat) => {
if (chat.project_id) return
if (chat.pinned) return

if (!chat.updated_at) {
todayChats.push(chat)
Expand Down
14 changes: 13 additions & 1 deletion app/components/layout/sidebar/app-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
NotePencilIcon,
X,
} from "@phosphor-icons/react"
import { Pin } from "lucide-react"
import { useParams, useRouter } from "next/navigation"
import { useMemo } from "react"
import { HistoryTrigger } from "../../history/history-trigger"
Expand All @@ -27,7 +28,7 @@ import { SidebarProject } from "./sidebar-project"
export function AppSidebar() {
const isMobile = useBreakpoint(768)
const { setOpenMobile } = useSidebar()
const { chats, isLoading } = useChats()
const { chats, pinnedChats, isLoading } = useChats()
const params = useParams<{ chatId: string }>()
const currentChatId = params.chatId

Expand Down Expand Up @@ -95,6 +96,17 @@ export function AppSidebar() {
<div className="h-full" />
) : hasChats ? (
<div className="space-y-5">
{pinnedChats.length > 0 && (
<div className="space-y-5">
<SidebarList
key="pinned"
title="Pinned"
icon={<Pin className="size-3" />}
items={pinnedChats}
currentChatId={currentChatId}
/>
</div>
)}
{groupedChats?.map((group) => (
<SidebarList
key={group.name}
Expand Down
18 changes: 17 additions & 1 deletion app/components/layout/sidebar/sidebar-item-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { useMessages } from "@/lib/chat-store/messages/provider"
import { useChatSession } from "@/lib/chat-store/session/provider"
import { Chat } from "@/lib/chat-store/types"
import { DotsThree, PencilSimple, Trash } from "@phosphor-icons/react"
import { Pin, PinOff } from "lucide-react"
import { useRouter } from "next/navigation"
import { useState } from "react"
import { DialogDeleteChat } from "./dialog-delete-chat"
Expand All @@ -28,7 +29,7 @@ export function SidebarItemMenu({
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false)
const router = useRouter()
const { deleteMessages } = useMessages()
const { deleteChat } = useChats()
const { deleteChat, togglePinned } = useChats()
const { chatId } = useChatSession()
const isMobile = useBreakpoint(768)

Expand All @@ -53,6 +54,21 @@ export function SidebarItemMenu({
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-40">
<DropdownMenuItem
className="cursor-pointer"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
togglePinned(chat.id, !chat.pinned)
}}
>
{chat.pinned ? (
<PinOff size={16} className="mr-2" />
) : (
<Pin size={16} className="mr-2" />
)}
{chat.pinned ? "Unpin" : "Pin"}
</DropdownMenuItem>
<DropdownMenuItem
className="cursor-pointer"
onClick={(e) => {
Expand Down
12 changes: 10 additions & 2 deletions app/components/layout/sidebar/sidebar-list.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
import { Chat } from "@/lib/chat-store/types"
import { ReactNode } from "react"
import { SidebarItem } from "./sidebar-item"

type SidebarListProps = {
title: string
icon?: ReactNode
items: Chat[]
currentChatId: string
}

export function SidebarList({ title, items, currentChatId }: SidebarListProps) {
export function SidebarList({
title,
icon,
items,
currentChatId,
}: SidebarListProps) {
return (
<div>
<h3 className="overflow-hidden px-2 pt-3 pb-2 text-xs font-semibold break-all text-ellipsis">
<h3 className="flex items-center gap-1 overflow-hidden px-2 pt-3 pb-2 text-xs font-semibold break-all text-ellipsis">
{icon && <span>{icon}</span>}
{title}
</h3>
<div className="space-y-0.5">
Expand Down
Loading