zhaoweijie efbf2a3eff feat/playground: 重构 playground组件
- 更新 Data 和 History组件的样式和布局
- 添加新的功能和交互,如热门搜索和智能体选择
- 优化组件性能和可维护性
2025-08-21 14:12:29 +08:00

288 lines
10 KiB
TypeScript

import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import {
PageAssitDatabase,
formatToChatHistory,
formatToMessage,
deleteByHistoryId,
updateHistory,
pinHistory,
getPromptById
} from "@/db"
import { Empty, Skeleton, Dropdown, Menu, Tooltip } from "antd"
import {
PencilIcon,
Trash2,
MoreVertical,
PinIcon,
PinOffIcon,
BotIcon
} from "lucide-react"
import { useNavigate } from "react-router-dom"
import { useTranslation } from "react-i18next"
import {
getLastUsedChatModel,
getLastUsedChatSystemPrompt,
lastUsedChatModelEnabled
} from "@/services/model-settings"
import { useState } from "react"
type Props = {
onClose: () => void
setMessages: (messages: any) => void
setHistory: (history: any) => void
setHistoryId: (historyId: string) => void
setSelectedModel: (model: string) => void
setSelectedSystemPrompt: (prompt: string) => void
setSystemPrompt: (prompt: string) => void
clearChat: () => void
temporaryChat: boolean
historyId: string
history: any
}
export const Sidebar = ({
onClose,
setMessages,
setHistory,
setHistoryId,
setSelectedModel,
setSelectedSystemPrompt,
clearChat,
historyId,
setSystemPrompt,
temporaryChat
}: Props) => {
const { t } = useTranslation(["option", "common"])
const client = useQueryClient()
const navigate = useNavigate()
const { data: chatHistories, status } = useQuery({
queryKey: ["fetchChatHistory"],
queryFn: async () => {
const db = new PageAssitDatabase()
const history = await db.getChatHistories()
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
}
})
const { mutate: deleteHistory } = useMutation({
mutationKey: ["deleteHistory"],
mutationFn: deleteByHistoryId,
onSuccess: (history_id) => {
client.invalidateQueries({
queryKey: ["fetchChatHistory"]
})
if (historyId === history_id) {
clearChat()
}
}
})
const { mutate: editHistory } = useMutation({
mutationKey: ["editHistory"],
mutationFn: async (data: { id: string; title: string }) => {
return await updateHistory(data.id, data.title)
},
onSuccess: () => {
client.invalidateQueries({
queryKey: ["fetchChatHistory"]
})
}
})
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 ${temporaryChat ? "pointer-events-none opacity-50" : ""}`}>
{status === "success" && chatHistories.length === 0 && (
<div className="flex justify-center items-center mt-20 overflow-hidden">
<Empty description={t("common:noHistory")} />
</div>
)}
{status === "pending" && (
<div className="flex justify-center items-center mt-5">
<Skeleton active paragraph={{ rows: 8 }} />
</div>
)}
{status === "error" && (
<div className="flex justify-center items-center">
<span className="text-red-500">Error loading history</span>
</div>
)}
{status === "success" && chatHistories.length > 0 && (
<div className="flex flex-col gap-2">
{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-center gap-3 relative rounded-md truncate hover:pr-4 group transition-opacity duration-300 ease-in-out border
hover:text-[#000000d9] hover:bg-[#f3f2ff] dark:hover:bg-[#2d2d2d] dark:border-gray-800
hover:[&_.more-vertical]:text-[#000000d9]
${historyId === chat.id ? 'text-[#000000d9] bg-[#f3f2ff] border-[#000000d9]' : 'dark:text-gray-100 text-gray-800'}
`}>
{chat?.message_source === "copilot" && (
<Tooltip title={t("common:sidebarChat")} placement="top">
<BotIcon className="size-3 text-green-500" />
</Tooltip>
)}
<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)
}
}
const lastUsedPrompt =
await getLastUsedChatSystemPrompt(chat.id)
if (lastUsedPrompt) {
if (lastUsedPrompt.prompt_id) {
const prompt = await getPromptById(
lastUsedPrompt.prompt_id
)
if (prompt) {
setSelectedSystemPrompt(lastUsedPrompt.prompt_id)
}
}
setSystemPrompt(lastUsedPrompt.prompt_content)
}
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={`group-hover:text-[#000000d9] w-4 h-4 more-vertical ${historyId === chat.id ? 'text-[#000000d9]' : ''}`} />
</button>
</Dropdown>
</div>
</div>
))}
</div>
</div>
))}
</div>
)}
</div>
)
}