refactor(layout): 重构布局组件并添加新功能

- 更新 Header 组件,增加项目标题和历史记录切换按钮
- 新增 DataNavigation 组件用于数据导航
- 添加 Playground 相关新组件,包括数据、场景、团队等信息展示
- 重构 Layout 组件,使用 Context 管理历史记录状态
- 更新 zh/option.json 文件,添加新的项目标题和对话相关翻译
This commit is contained in:
zhaoweijie
2025-08-19 16:20:37 +08:00
parent ef0e315bdc
commit 1104fb2733
33 changed files with 1008 additions and 258 deletions

View File

@@ -0,0 +1,38 @@
import React from "react";
import { Typography, Button } from "antd";
import { AcademicCapIcon, ChevronRightIcon } from "@heroicons/react/24/outline";
const { Title } = Typography;
type Props = {
Header: React.ReactNode;
showButton?: boolean;
onClick?: () => void;
};
export const DataNavigation: React.FC<Props> = ({ Header, showButton = true, onClick }) => {
return (
<div className="flex items-center justify-between bg-white dark:bg-gray-800 rounded-lg mb-3">
{/* 左侧部分 */}
<div className="flex items-center">
<Title
level={4}
style={{ marginBottom: 0, color: "#1F2937", fontSize: "16px" }}>
{Header}
</Title>
</div>
{/* 右侧部分 */}
{showButton && (
<Button
color="default"
variant="link"
style={{ gap: 4 }}
onClick={onClick}>
<span className="text-sm"></span>
<ChevronRightIcon className="w-4 h-4" />
</Button>
)}
</div>
)
};

View File

@@ -12,7 +12,7 @@ import { preprocessLaTeX } from "@/utils/latex"
function Markdown({
message,
className = "prose break-words dark:prose-invert prose-p:leading-relaxed prose-pre:p-0 dark:prose-dark"
className = "prose-lg break-words dark:prose-invert prose-p:leading-relaxed prose-pre:p-0 dark:prose-dark"
}: {
message: string
className?: string

View File

@@ -0,0 +1,175 @@
import React from "react"
import { DataNavigation } from "@/components/Common/DataNavigation.tsx"
import { Card, Drawer, List } from "antd"
import { useCallback, useState } from "react"
export const PlaygroundData = () => {
// 模拟数据
const data: {
title: string
description: string
time: string
metadata?: string
}[] = [
{
title: "2019-2024年黄海清浅海域中河湖代数生物物种数据集",
description:
"数字对象标识: CSTR:13452.11.01.11.2021.242 国家海洋科学数据中心",
time: "包括2019年8月2021年8月和2024年6月",
metadata: "热 榜 第"
},
{
title: "祁连山老虎沟大本营10米气象每日值数据集V1.02018-2023",
description:
"中国科学院西北生态环境资源研究院2021年8月3日发布2021年8月3日20:48更新",
time: "包括2019年8月2021年8月和2024年6月",
metadata: "热 榜 第"
},
{
title: "李嘉图为研究老虎沟大本营2014-2018年...",
description:
"中国科学院西北生态环境资源研究院2021年8月3日发布2021年8月3日20:48更新",
time: "包括2019年8月2021年8月和2024年6月",
metadata: "热 榜 第"
},
{
title: "青海玉树B1区俄日矿勘探数据2017-2023",
description:
"数字中国集团CSTR:3260.11.1528414774204895456DT2023年地质勘探补充调查",
time: "包括2019年8月2021年8月和2024年6月",
metadata: "热 榜 第"
}
]
for (let i = 0; i < 10; i++) {
data.push({
title: "中国资源环境网",
description: "中国资源环境网2021年8月3日发布2021年8月3日20:48更新",
time: "包括2019年8月2021年8月和2024年6月"
})
}
const [open, setOpen] = useState(false)
const showDrawer = () => {
setOpen(true)
}
const onClose = () => {
setOpen(false)
}
return (
<div className="flex flex-col h-full overflow-y-hidden">
{/* 数据导航 */}
<DataNavigation
Header={
<div className="flex items-center text-[#1d4eD8] gap-1">
<svg
xmlns="http://www.w3.org/2000/svg"
className="styles__StyledSVGIconPathComponent-sc-i3aj97-0 bxMexi svg-icon-path-icon"
viewBox="0 0 32 32"
width="20"
height="20">
<defs></defs>
<g>
<circle cx="22" cy="24" r="2" fill="rgb(29, 78, 216)"></circle>
<path
fill="rgb(29, 78, 216)"
d="M29.777 23.479A8.64 8.64 0 0 0 22 18a8.64 8.64 0 0 0-7.777 5.479L14 24l.223.522A8.64 8.64 0 0 0 22 30a8.64 8.64 0 0 0 7.777-5.478L30 24zM22 28a4 4 0 1 1 4-4a4.005 4.005 0 0 1-4 4M7 17h5v2H7zm0-5h12v2H7zm0-5h12v2H7z"></path>
<path
fill="rgb(29, 78, 216)"
d="M22 2H4a2.006 2.006 0 0 0-2 2v24a2.006 2.006 0 0 0 2 2h8v-2H4V4h18v11h2V4a2.006 2.006 0 0 0-2-2"></path>
</g>
</svg>
</div>
}
onClick={showDrawer}
/>
{/* 数据列表 */}
<div className="space-y-1.5 flex-1 overflow-y-auto">
{data.slice(0, 3).map((item, index) => (
<Card key={index} hoverable className="[&>*:first-child]:!p-3 h-[148px]">
<div className="flex flex-col gap-0.5">
<h3 className="text-sm font-medium text-gray-900 line-clamp-2">
{item.title}
</h3>
<p className="text-xs text-gray-500 line-clamp-2">
{item.description}
</p>
<p className="text-gray-700 truncate">{item.time}</p>
{item.metadata && (
<div className="text-green-500 text-xs px-2 py-1 rounded-full flex items-center gap-1">
<svg
xmlns="http://www.w3.org/2000/svg"
className="styles__StyledSVGIconPathComponent-sc-i3aj97-0 bxMexi svg-icon-path-icon"
viewBox="0 0 24 24"
width="12"
height="12">
<defs></defs>
<g>
<path
fill="rgb(34, 197, 94)"
d="m16 11.78l4.24-7.33l1.73 1l-5.23 9.05l-6.51-3.75L5.46 19H22v2H2V3h2v14.54L9.5 8z"></path>
</g>
</svg>
{item.metadata}
</div>
)}
</div>
</Card>
))}
</div>
{/* 抽屉 */}
<Drawer
title="相关数据"
closable={{ "aria-label": "Close Button" }}
onClose={onClose}
open={open}
width={600}>
<List
itemLayout="vertical"
dataSource={data}
renderItem={(item, index) => (
<List.Item>
<List.Item.Meta
title={
<h3 className="text-sm font-medium text-gray-900">
{item.title}
</h3>
}
description={
<div className="space-y-1">
<p className="text-xs text-gray-500">{item.description}</p>
<p className="text-gray-700">{item.time}</p>
{item.metadata && (
<div className="text-green-500 text-xs px-2 py-1 rounded-full flex items-center gap-1">
<svg
xmlns="http://www.w3.org/2000/svg"
className="styles__StyledSVGIconPathComponent-sc-i3aj97-0 bxMexi svg-icon-path-icon"
viewBox="0 0 24 24"
width="12"
height="12">
<defs></defs>
<g>
<path
fill="rgb(34, 197, 94)"
d="m16 11.78l4.24-7.33l1.73 1l-5.23 9.05l-6.51-3.75L5.46 19H22v2H2V3h2v14.54L9.5 8z"></path>
</g>
</svg>
{item.metadata}
</div>
)}
</div>
}
/>
</List.Item>
)}
/>
</Drawer>
</div>
)
}

View File

@@ -0,0 +1,84 @@
import { Sidebar } from "@/components/Option/Sidebar.tsx"
import React, { useContext, useState } from "react"
import { useMessageOption } from "@/hooks/useMessageOption.tsx"
import { useStoreChatModelSettings } from "@/store/model.tsx"
import { Card, Tooltip } from "antd"
import { PageAssitDatabase } from "@/db"
import { EraserIcon } from "lucide-react"
import { useTranslation } from "react-i18next"
import { useQueryClient } from "@tanstack/react-query"
import { HistoryContext } from "@/components/Layouts/Layout.tsx"
export const PlaygroundHistory = () => {
const { setSystemPrompt } = useStoreChatModelSettings()
const { show, setShow } = useContext(HistoryContext)
const {
setMessages,
setHistory,
setHistoryId,
historyId,
clearChat,
setSelectedModel,
temporaryChat,
setSelectedSystemPrompt
} = useMessageOption()
const { t } = useTranslation(["option", "common", "settings"])
const queryClient = useQueryClient()
return (
<Card
className={`flex flex-col [&>:nth-child(2)]:flex-1 [&>:nth-child(2)]:overflow-y-auto w-[300px] h-full pt-16 pb-5 transition-all duration-300 ease-in-out transform ${
show
? 'opacity-100 translate-x-0'
: 'opacity-0 -translate-x-full absolute'
}`}
style={{ paddingTop: "4rem" }}
title={
<div className="flex items-center justify-between w-full">
{t("sidebarTitle")}
<Tooltip
title={t("settings:generalSettings.system.deleteChatHistory.label")}
placement="right">
<button
onClick={async () => {
const confirm = window.confirm(
t("settings:generalSettings.system.deleteChatHistory.confirm")
)
if (confirm) {
const db = new PageAssitDatabase()
await db.deleteAllChatHistory()
await queryClient.invalidateQueries({
queryKey: ["fetchChatHistory"]
})
clearChat()
}
}}
className="text-gray-600 hover:text-gray-800 dark:text-gray-300 dark:hover:text-gray-100">
<EraserIcon className="size-5" />
</button>
</Tooltip>
</div>
}>
<div className="overflow-y-auto">
<Sidebar
onClose={() => setShow(true)}
setMessages={setMessages}
setHistory={setHistory}
setHistoryId={setHistoryId}
setSelectedModel={setSelectedModel}
setSelectedSystemPrompt={setSelectedSystemPrompt}
clearChat={clearChat}
historyId={historyId}
setSystemPrompt={setSystemPrompt}
temporaryChat={temporaryChat}
history={history}
/>
</div>
</Card>
)
}

View File

@@ -0,0 +1,95 @@
import React from 'react';
const SuccessIcon = () => {
return (<svg
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="w-full h-full text-green-500">
<path
d="M9 12L11 14L15 10M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>)
}
const LoadingIcon = () => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
className="styles__StyledSVGIconPathComponent-sc-i3aj97-0 bxMexi svg-icon-path-icon"
viewBox="0 0 24 24"
width="18"
height="18">
<defs></defs>
<g>
<path
fill="rgb(59, 130, 246)"
d="M13 2.03v2.02c4.39.54 7.5 4.53 6.96 8.92c-.46 3.64-3.32 6.53-6.96 6.96v2c5.5-.55 9.5-5.43 8.95-10.93c-.45-4.75-4.22-8.5-8.95-8.97m-2 .03c-1.95.19-3.81.94-5.33 2.2L7.1 5.74c1.12-.9 2.47-1.48 3.9-1.68zM4.26 5.67A9.9 9.9 0 0 0 2.05 11h2c.19-1.42.75-2.77 1.64-3.9zM2.06 13c.2 1.96.97 3.81 2.21 5.33l1.42-1.43A8 8 0 0 1 4.06 13zm5.04 5.37l-1.43 1.37A10 10 0 0 0 11 22v-2a8 8 0 0 1-3.9-1.63M12.5 7v5.25l4.5 2.67l-.75 1.23L11 13V7z"></path>
</g>
</svg>
)
}
export const PlaygroundIodRelevant: React.FC = () => {
const data = [
{
title: "已在29个科学数据中心的50万个科学数据集中进行搜索",
description: "已发现4个数据集",
status: "success"
},
{
title: "已在100万篇论文、2800个科创场景中进行搜索",
description: "已发现4个数据集",
status: "success"
},
{
title: "正在1000位智库专家、12万个创新机构中进行搜索",
status: "loading"
},
]
for (let i = 0; i < 10; i++) {
data.push({
title: "已在29个科学数据中心的50万个科学数据集中进行搜索" + i,
description: "已发现4个数据集",
status: "success"
})
}
return (
<div className="flex flex-col h-full">
{/* Header */}
<div className="flex justify-between items-center mb-4">
<h2 className="text-lg font-semibold text-gray-900">
</h2>
<span className="text-sm text-blue-600 font-medium">{data.length}</span>
</div>
{/* Content */}
<div className="space-y-3 flex-1 overflow-y-auto">
{
data.map((item, index) => (
<div className="flex items-start space-x-3">
<div className="w-5 h-5 mt-1 flex-shrink-0">
{item.status === "success" ? <SuccessIcon /> : <LoadingIcon />}
</div>
<div className="flex-1">
<p className="text-sm text-gray-700">
{item.title}
</p>
{item.description && <p className="text-xs text-gray-500 mt-1">{item.description}</p>}
</div>
</div>
))
}
</div>
</div>
)
}

View File

@@ -58,7 +58,7 @@ export const PlaygroundMessage = (props: Props) => {
const { t } = useTranslation("common")
const { cancel, isSpeaking, speak } = useTTS()
return (
<div className="group relative flex w-full max-w-3xl flex-col items-end justify-center pb-2 md:px-4 lg:w-4/5 text-gray-800 dark:text-gray-100">
<div className="group relative flex w-full flex-col items-end justify-center pb-2 md:px-4 text-gray-800 dark:text-gray-100">
{/* <div className="text-base md:max-w-2xl lg:max-w-xl xl:max-w-3xl flex lg:px-0 m-auto w-full"> */}
<div className="flex flex-row gap-4 md:gap-6 my-2 m-auto w-full">
<div className="w-8 flex flex-col relative items-end">
@@ -138,7 +138,7 @@ export const PlaygroundMessage = (props: Props) => {
</>
) : (
<p
className={`prose dark:prose-invert whitespace-pre-line prose-p:leading-relaxed prose-pre:p-0 dark:prose-dark ${
className={`prose-lg dark:prose-invert whitespace-pre-line prose-p:leading-relaxed prose-pre:p-0 dark:prose-dark ${
props.message_type &&
"italic text-gray-500 dark:text-gray-400 text-sm"
}`}>

View File

@@ -0,0 +1,142 @@
import React, { useState } from "react"
import { DataNavigation } from "@/components/Common/DataNavigation.tsx"
import { Card, Drawer, List } from "antd"
export const PlaygroundScene = () => {
// 模拟数据
const data = [
{
title: "绿色化工工艺项目",
description:
"基于生物基原料采用repeal2.0可降解材料技术,开发新型环保材料。",
demander: "奥赛康药业 供方美国Propella公司"
},
{
title: "智能农业解决方案",
description: "利用物联网技术,实现精准农业管理,提高农作物产量。",
demander: "奥赛康药业 供方美国Propella公司"
},
{
title: "新能源汽车电池技术",
description: "研发高能量密度、长寿命的新型电池材料,推动电动汽车发展。",
demander: "奥赛康药业 供方美国Propella公司"
},
{
title: "碳捕集与封存技术",
description: "开发高效的碳捕集技术,减少工业排放,助力碳中和目标。",
demander: "奥赛康药业 供方美国Propella公司"
}
]
for (let i = 0; i < 10; i++) {
data.push({
title: "开发新型电池材料",
description: "研发高能量密度、长寿命的新型电池材料,推动电动汽车发展。",
demander: "奥赛康药业 供方美国Propella公司"
})
}
const [open, setOpen] = useState(false)
const showDrawer = () => {
setOpen(true)
}
const onClose = () => {
setOpen(false)
}
return (
<div className="h-full overflow-y-hidden flex flex-col">
{/* 数据导航 */}
<DataNavigation
Header={
<div className="flex items-center text-[#15803d] gap-1">
<svg
xmlns="http://www.w3.org/2000/svg"
className="styles__StyledSVGIconPathComponent-sc-i3aj97-0 bxMexi svg-icon-path-icon"
viewBox="0 0 32 32"
width="20"
height="20">
<defs></defs>
<g>
<path
fill="rgb(21, 128, 61)"
d="M16 18H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2M6 6v10h10V6zm20 6v4h-4v-4zm0-2h-4a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2m0 12v4h-4v-4zm0-2h-4a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2m-10 2v4h-4v-4zm0-2h-4a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2"></path>
</g>
</svg>
</div>
}
onClick={showDrawer}
/>
{/* 场景列表 */}
<div className="space-y-1.5 flex-1 overflow-y-auto">
{data.slice(0,3).map((item, index) => (
<Card key={index} hoverable className="[&>*:first-child]:!p-4 h-[148px]" >
<div className="flex flex-col gap-0.5">
<h3 className="text-sm font-medium text-gray-900 line-clamp-2">
{item.title}
</h3>
<p className="flex items-center gap-1.5">
<span className="inline-block bg-blue-100 text-green-800 text-xs px-2 py-1 rounded-full">
</span>
<span className="inline-block bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
</span>
</p>
<p className="text-xs text-gray-500 truncate">{item.demander}</p>
<span className="text-gray-700 line-clamp-2">
{item.description}
</span>
</div>
</Card>
))}
</div>
{/* 抽屉 */}
<Drawer
title="相关场景"
closable={{ "aria-label": "Close Button" }}
onClose={onClose}
open={open}
width={600}>
<List
itemLayout="vertical"
dataSource={data}
renderItem={(item, index) => (
<List.Item>
<List.Item.Meta
title={
<h3 className="text-sm font-medium text-gray-900">
{item.title}
</h3>
}
description={
<div className="space-y-1">
<p className="flex items-center gap-1.5">
<span className="inline-block bg-blue-100 text-green-800 text-xs px-2 py-1 rounded-full">
</span>
<span className="inline-block bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
</span>
</p>
<p className="text-xs text-gray-500">
{item.demander}
</p>
<span className="text-gray-700">
{item.description}
</span>
</div>
}
/>
</List.Item>
)}
/>
</Drawer>
</div>
)
}

View File

@@ -0,0 +1,136 @@
import React, { useState } from "react"
import { DataNavigation } from "@/components/Common/DataNavigation.tsx"
import { Card, Drawer, List } from "antd"
export const PlaygroundTeam = () => {
// 模拟数据
const data = [
{
title: "绿色化工工艺项目",
description:
"基于生物基原料采用repeal2.0可降解材料技术,开发新型环保材料。",
demander: "奥赛康药业 供方美国Propella公司"
},
{
title: "智能农业解决方案",
description: "利用物联网技术,实现精准农业管理,提高农作物产量。",
demander: "奥赛康药业 供方美国Propella公司"
},
{
title: "新能源汽车电池技术",
description: "研发高能量密度、长寿命的新型电池材料,推动电动汽车发展。",
demander: "奥赛康药业 供方美国Propella公司"
},
{
title: "碳捕集与封存技术",
description: "开发高效的碳捕集技术,减少工业排放,助力碳中和目标。",
demander: "奥赛康药业 供方美国Propella公司"
}
]
for (let i = 0; i < 10; i++) {
data.push({
title: "开发新型电池材料",
description: "研发高能量密度、长寿命的新型电池材料,推动电动汽车发展。",
demander: "奥赛康药业 供方美国Propella公司"
})
}
const [open, setOpen] = useState(false)
const showDrawer = () => {
setOpen(true)
}
const onClose = () => {
setOpen(false)
}
return (
<div className="h-full overflow-y-hidden flex flex-col">
{/* 数据导航 */}
<DataNavigation
Header={
<div className="flex items-center text-[#15803d] gap-1">
<svg
xmlns="http://www.w3.org/2000/svg"
className="styles__StyledSVGIconPathComponent-sc-i3aj97-0 bxMexi svg-icon-path-icon"
viewBox="0 0 32 32"
width="20"
height="20">
<defs></defs>
<g>
<path
fill="rgb(21, 128, 61)"
d="M16 18H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2M6 6v10h10V6zm20 6v4h-4v-4zm0-2h-4a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2m0 12v4h-4v-4zm0-2h-4a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2m-10 2v4h-4v-4zm0-2h-4a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2"></path>
</g>
</svg>
</div>
}
onClick={showDrawer}
/>
{/* 场景列表 */}
<div className="grid grid-cols-2 gap-3">
{data.slice(0,2).map((item, index) => (
<Card key={index} hoverable className="[&>*:first-child]:!p-3" >
<div className="flex flex-col gap-0.5">
<h3 className="text-sm font-medium text-gray-900 line-clamp-2">
{item.title}
</h3>
<p className="flex items-center gap-1.5">
<span className="inline-block bg-blue-100 text-green-800 text-xs px-2 py-1 rounded-full">
</span>
<span className="inline-block bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
</span>
</p>
<p className="text-xs text-gray-500 line-clamp-2">{item.description}</p>
</div>
</Card>
))}
</div>
{/* 抽屉 */}
<Drawer
title="相关团队"
closable={{ "aria-label": "Close Button" }}
onClose={onClose}
open={open}
width={600}>
<List
itemLayout="vertical"
dataSource={data}
renderItem={(item, index) => (
<List.Item>
<List.Item.Meta
title={
<h3 className="text-sm font-medium text-gray-900">
{item.title}
</h3>
}
description={
<div className="space-y-1">
<p className="flex items-center gap-1.5">
<span className="inline-block bg-blue-100 text-green-800 text-xs px-2 py-1 rounded-full">
</span>
<span className="inline-block bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
</span>
</p>
<p className="text-xs text-gray-500">
{item.description}
</p>
</div>
}
/>
</List.Item>
)}
/>
</Drawer>
</div>
)
}

View File

@@ -0,0 +1,46 @@
import { DataNavigation } from "@/components/Common/DataNavigation.tsx"
import { Card, Descriptions, DescriptionsProps, Drawer, List, Spin } from "antd"
import { useCallback, useMemo, useState } from "react"
import { useMessageOption } from "@/hooks/useMessageOption.tsx"
import { useStoreMessageOption } from "@/store/option.tsx"
export const PlaygroundTokenStatistics = () => {
const { currentMeteringEntry } = useStoreMessageOption()
const items = useMemo<DescriptionsProps["items"]>(() => {
const { data } = currentMeteringEntry
return [
// {
// key: "relatedDataCount",
// label: "关联数据个数",
// children: data.relatedDataCount
// },
{
key: "iodTokenCount",
label: "数联网引用token总数",
children: data.iodTokenCount ?? 0
},
{
key: "modelInputTokenCount",
label: "大模型输入token数量",
children: data.modelInputTokenCount ?? 0
},
{
key: "modelOutputTokenCount",
label: "大模型输出token数量",
children: data.modelOutputTokenCount ?? 0
}
]
}, [currentMeteringEntry])
return (
<Card
style={{ marginBottom: "1rem" }}
className="h-full"
title={<DataNavigation title="Token统计" showButton={false} />}>
<Spin spinning={currentMeteringEntry.loading}>
<Descriptions layout="horizontal" items={items} column={2} />
</Spin>
</Card>
)
}