feat: add pin/unpin functionality to chat history
Adds pin/unpin functionality to the chat history sidebar, allowing users to keep important conversations readily accessible. This improves user experience and helps organize past interactions. This feature includes: - Pin/unpin buttons in the chat history sidebar. - Updated database schema to include `is_pinned` field for chat history items. - Localized translations for pin/unpin actions. - Updated UI to display pinned items at the top of the list.
This commit is contained in:
@@ -93,7 +93,7 @@ export const KnowledgeSettings = () => {
|
||||
key: "action",
|
||||
render: (text: string, record: any) => (
|
||||
<div className="flex gap-4">
|
||||
<Tooltip title={t("tooltip.delete")}>
|
||||
<Tooltip title={t("common:delete")}>
|
||||
<button
|
||||
disabled={isDeleting}
|
||||
onClick={() => {
|
||||
|
||||
@@ -4,11 +4,18 @@ import {
|
||||
formatToChatHistory,
|
||||
formatToMessage,
|
||||
deleteByHistoryId,
|
||||
updateHistory
|
||||
updateHistory,
|
||||
pinHistory
|
||||
} from "@/db"
|
||||
import { Empty, Skeleton } from "antd"
|
||||
import { Empty, Skeleton, Dropdown, Menu } from "antd"
|
||||
import { useMessageOption } from "~/hooks/useMessageOption"
|
||||
import { PencilIcon, Trash2 } from "lucide-react"
|
||||
import {
|
||||
PencilIcon,
|
||||
Trash2,
|
||||
MoreVertical,
|
||||
PinIcon,
|
||||
PinOffIcon
|
||||
} from "lucide-react"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import {
|
||||
@@ -38,7 +45,46 @@ export const Sidebar = ({ onClose }: Props) => {
|
||||
queryFn: async () => {
|
||||
const db = new PageAssitDatabase()
|
||||
const history = await db.getChatHistories()
|
||||
return history
|
||||
|
||||
const now = new Date()
|
||||
const today = new Date(now.setHours(0, 0, 0, 0))
|
||||
const yesterday = new Date(today)
|
||||
yesterday.setDate(yesterday.getDate() - 1)
|
||||
const lastWeek = new Date(today)
|
||||
lastWeek.setDate(lastWeek.getDate() - 7)
|
||||
|
||||
const pinnedItems = history.filter((item) => item.is_pinned)
|
||||
const todayItems = history.filter(
|
||||
(item) => !item.is_pinned && new Date(item?.createdAt) >= today
|
||||
)
|
||||
const yesterdayItems = history.filter(
|
||||
(item) =>
|
||||
!item.is_pinned &&
|
||||
new Date(item?.createdAt) >= yesterday &&
|
||||
new Date(item?.createdAt) < today
|
||||
)
|
||||
const lastWeekItems = history.filter(
|
||||
(item) =>
|
||||
!item.is_pinned &&
|
||||
new Date(item?.createdAt) >= lastWeek &&
|
||||
new Date(item?.createdAt) < yesterday
|
||||
)
|
||||
const olderItems = history.filter(
|
||||
(item) => !item.is_pinned && new Date(item?.createdAt) < lastWeek
|
||||
)
|
||||
|
||||
const groups = []
|
||||
|
||||
if (pinnedItems.length)
|
||||
groups.push({ label: "pinned", items: pinnedItems })
|
||||
if (todayItems.length) groups.push({ label: "today", items: todayItems })
|
||||
if (yesterdayItems.length)
|
||||
groups.push({ label: "yesterday", items: yesterdayItems })
|
||||
if (lastWeekItems.length)
|
||||
groups.push({ label: "last7days", items: lastWeekItems })
|
||||
if (olderItems.length) groups.push({ label: "older", items: olderItems })
|
||||
|
||||
return groups
|
||||
}
|
||||
})
|
||||
|
||||
@@ -67,6 +113,18 @@ export const Sidebar = ({ onClose }: Props) => {
|
||||
}
|
||||
})
|
||||
|
||||
const { mutate: pinChatHistory, isPending: pinLoading } = useMutation({
|
||||
mutationKey: ["pinHistory"],
|
||||
mutationFn: async (data: { id: string; is_pinned: boolean }) => {
|
||||
return await pinHistory(data.id, data.is_pinned)
|
||||
},
|
||||
onSuccess: () => {
|
||||
client.invalidateQueries({
|
||||
queryKey: ["fetchChatHistory"]
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="overflow-y-auto z-99">
|
||||
{status === "success" && chatHistories.length === 0 && (
|
||||
@@ -86,51 +144,99 @@ export const Sidebar = ({ onClose }: Props) => {
|
||||
)}
|
||||
{status === "success" && chatHistories.length > 0 && (
|
||||
<div className="flex flex-col gap-2">
|
||||
{chatHistories.map((chat, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex py-2 px-2 items-start gap-3 relative rounded-md truncate hover:pr-4 group transition-opacity duration-300 ease-in-out bg-gray-100 dark:bg-[#232222] dark:text-gray-100 text-gray-800 border hover:bg-gray-200 dark:hover:bg-[#2d2d2d] dark:border-gray-800">
|
||||
<button
|
||||
className="flex-1 overflow-hidden break-all text-start truncate w-full"
|
||||
onClick={async () => {
|
||||
const db = new PageAssitDatabase()
|
||||
const history = await db.getChatHistory(chat.id)
|
||||
setHistoryId(chat.id)
|
||||
setHistory(formatToChatHistory(history))
|
||||
setMessages(formatToMessage(history))
|
||||
const isLastUsedChatModel = await lastUsedChatModelEnabled()
|
||||
if (isLastUsedChatModel) {
|
||||
const currentChatModel = await getLastUsedChatModel(chat.id)
|
||||
if (currentChatModel) {
|
||||
setSelectedModel(currentChatModel)
|
||||
}
|
||||
}
|
||||
navigate("/")
|
||||
onClose()
|
||||
}}>
|
||||
<span className="flex-grow truncate">{chat.title}</span>
|
||||
</button>
|
||||
<div className="flex flex-row gap-3">
|
||||
<button
|
||||
onClick={() => {
|
||||
const newTitle = prompt(t("editHistoryTitle"), chat.title)
|
||||
|
||||
if (newTitle) {
|
||||
editHistory({ id: chat.id, title: newTitle })
|
||||
}
|
||||
}}
|
||||
className="text-gray-500 dark:text-gray-400 opacity-80">
|
||||
<PencilIcon className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
if (!confirm(t("deleteHistoryConfirmation"))) return
|
||||
deleteHistory(chat.id)
|
||||
}}
|
||||
className="text-red-500 dark:text-red-400 opacity-80">
|
||||
<Trash2 className=" w-4 h-4 " />
|
||||
</button>
|
||||
{chatHistories.map((group, groupIndex) => (
|
||||
<div key={groupIndex}>
|
||||
<h3 className="px-2 text-sm font-medium text-gray-500">
|
||||
{t(`common:date:${group.label}`)}
|
||||
</h3>
|
||||
<div className="flex flex-col gap-2 mt-2">
|
||||
{group.items.map((chat, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex py-2 px-2 items-start gap-3 relative rounded-md truncate hover:pr-4 group transition-opacity duration-300 ease-in-out bg-gray-100 dark:bg-[#232222] dark:text-gray-100 text-gray-800 border hover:bg-gray-200 dark:hover:bg-[#2d2d2d] dark:border-gray-800">
|
||||
<button
|
||||
className="flex-1 overflow-hidden break-all text-start truncate w-full"
|
||||
onClick={async () => {
|
||||
const db = new PageAssitDatabase()
|
||||
const history = await db.getChatHistory(chat.id)
|
||||
setHistoryId(chat.id)
|
||||
setHistory(formatToChatHistory(history))
|
||||
setMessages(formatToMessage(history))
|
||||
const isLastUsedChatModel =
|
||||
await lastUsedChatModelEnabled()
|
||||
if (isLastUsedChatModel) {
|
||||
const currentChatModel = await getLastUsedChatModel(
|
||||
chat.id
|
||||
)
|
||||
if (currentChatModel) {
|
||||
setSelectedModel(currentChatModel)
|
||||
}
|
||||
}
|
||||
navigate("/")
|
||||
onClose()
|
||||
}}>
|
||||
<span className="flex-grow truncate">{chat.title}</span>
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<Dropdown
|
||||
overlay={
|
||||
<Menu>
|
||||
<Menu.Item
|
||||
key="pin"
|
||||
icon={
|
||||
chat.is_pinned ? (
|
||||
<PinOffIcon className="w-4 h-4" />
|
||||
) : (
|
||||
<PinIcon className="w-4 h-4" />
|
||||
)
|
||||
}
|
||||
onClick={() =>
|
||||
pinChatHistory({
|
||||
id: chat.id,
|
||||
is_pinned: !chat.is_pinned
|
||||
})
|
||||
}
|
||||
disabled={pinLoading}>
|
||||
{chat.is_pinned
|
||||
? t("common:unpin")
|
||||
: t("common:pin")}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key="edit"
|
||||
icon={<PencilIcon className="w-4 h-4" />}
|
||||
onClick={() => {
|
||||
const newTitle = prompt(
|
||||
t("editHistoryTitle"),
|
||||
chat.title
|
||||
)
|
||||
if (newTitle) {
|
||||
editHistory({ id: chat.id, title: newTitle })
|
||||
}
|
||||
}}>
|
||||
{t("common:edit")}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
key="delete"
|
||||
icon={<Trash2 className="w-4 h-4" />}
|
||||
danger
|
||||
onClick={() => {
|
||||
if (!confirm(t("deleteHistoryConfirmation")))
|
||||
return
|
||||
deleteHistory(chat.id)
|
||||
}}>
|
||||
{t("common:delete")}
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
}
|
||||
trigger={["click"]}
|
||||
placement="bottomRight">
|
||||
<button className="text-gray-500 dark:text-gray-400 opacity-80 hover:opacity-100">
|
||||
<MoreVertical className="w-4 h-4" />
|
||||
</button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user