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

@@ -1,8 +1,14 @@
import React from "react"
import { Card } from "antd"
import { PlaygroundForm } from "./PlaygroundForm"
import { PlaygroundChat } from "./PlaygroundChat"
import { useMessageOption } from "@/hooks/useMessageOption"
import { webUIResumeLastChat } from "@/services/app"
import { PlaygroundData } from '@/components/Common/Playground/Data.tsx'
import { PlaygroundScene } from "@/components/Common/Playground/Scene.tsx"
import {
formatToChatHistory,
formatToMessage,
@@ -13,6 +19,11 @@ import { getLastUsedChatSystemPrompt } from "@/services/model-settings"
import { useStoreChatModelSettings } from "@/store/model"
import { useSmartScroll } from "@/hooks/useSmartScroll"
import { ChevronDown } from "lucide-react"
import { PlaygroundTeam } from "@/components/Common/Playground/Team.tsx"
import { PlaygroundTokenStatistics } from "@/components/Common/Playground/TokenStatistics.tsx"
import { PlaygroundHistory } from "@/components/Common/Playground/History.tsx"
import { PlaygroundIodRelevant } from "@/components/Common/Playground/IodRelevant.tsx"
export const Playground = () => {
const drop = React.useRef<HTMLDivElement>(null)
@@ -132,26 +143,43 @@ export const Playground = () => {
return (
<div
ref={drop}
className={`relative flex h-full flex-col items-center ${
className={`relative flex gap-3 h-full items-center ${
dropState === "dragging" ? "bg-gray-100 dark:bg-gray-800" : ""
} bg-white dark:bg-[#171717]`}>
<div
ref={containerRef}
className="custom-scrollbar bg-bottom-mask-light dark:bg-bottom-mask-dark mask-bottom-fade will-change-mask flex h-full w-full flex-col items-center overflow-x-hidden overflow-y-auto px-5">
<PlaygroundChat />
<PlaygroundHistory />
<div className="relative h-full flex-1 prose-lg flex justify-center [&>*]:max-w-[848px]">
<div
ref={containerRef}
className="custom-scrollbar bg-bottom-mask-light dark:bg-bottom-mask-dark mask-bottom-fade will-change-mask flex h-full w-full flex-col items-center overflow-x-hidden overflow-y-auto px-5">
<PlaygroundChat />
</div>
<div className="absolute bottom-0 w-full">
{!isAtBottom && (
<div className="absolute bottom-36 z-20 left-0 right-0 flex justify-center">
<button
onClick={scrollToBottom}
className="bg-gray-50 shadow border border-gray-200 dark:border-none dark:bg-white/20 p-1.5 rounded-full pointer-events-auto">
<ChevronDown className="size-4 text-gray-600 dark:text-gray-300" />
</button>
</div>
)}
<PlaygroundForm dropedFile={dropedFile} />
</div>
</div>
<div className="absolute bottom-0 w-full">
{!isAtBottom && (
<div className="fixed bottom-36 z-20 left-0 right-0 flex justify-center">
<button
onClick={scrollToBottom}
className="bg-gray-50 shadow border border-gray-200 dark:border-none dark:bg-white/20 p-1.5 rounded-full pointer-events-auto">
<ChevronDown className="size-4 text-gray-600 dark:text-gray-300" />
</button>
{messages.length && (
<div className="w-1/4 h-full grid grid-rows-[auto_530px_165px] pt-16 pr-5 pb-0 border-l border-gray-200" style={{"paddingTop": "4rem"}}>
<div className="w-full overflow-y-auto border-gray-200 border-b p-3">
<PlaygroundIodRelevant />
</div>
)}
<PlaygroundForm dropedFile={dropedFile} />
</div>
<div className="w-full grid grid-cols-2 gap-3 custom-scrollbar border-gray-200 border-b p-3">
<PlaygroundData />
<PlaygroundScene />
</div>
<div className="w-full p-3 pb-0">
<PlaygroundTeam />
</div>
</div>
)}
</div>
)
}

View File

@@ -20,7 +20,7 @@ export const PlaygroundChat = () => {
<>
<div className="relative flex w-full flex-col items-center pt-16 pb-4">
{messages.length === 0 && (
<div className="mt-32 w-full">
<div className="mt-3 w-full">
<PlaygroundEmpty />
</div>
)}

View File

@@ -1,130 +1,113 @@
import { cleanUrl } from "@/libs/clean-url"
import { useStorage } from "@plasmohq/storage/hook"
import { useQuery } from "@tanstack/react-query"
import { RotateCcw } from "lucide-react"
import { useEffect, useState } from "react"
import { Trans, useTranslation } from "react-i18next"
import {
getOllamaURL,
isOllamaRunning,
setOllamaURL as saveOllamaURL
} from "~/services/ollama"
import { Card, Col, Row } from "antd"
import RocketSvg from '@/assets/icons/rocket.svg'
import BulbSvg from '@/assets/icons/bulb.svg'
import EyeSvg from '@/assets/icons/eye.svg'
import ASvg from '@/assets/icons/a.svg'
import BSvg from '@/assets/icons/b.svg'
import CSvg from '@/assets/icons/c.svg'
import DSvg from '@/assets/icons/d.svg'
import ESvg from '@/assets/icons/e.svg'
import FSvg from '@/assets/icons/f.svg'
import { useMessageOption } from "@/hooks/useMessageOption.tsx"
import { useMutation, useQueryClient } from "@tanstack/react-query"
export const PlaygroundEmpty = () => {
const [ollamaURL, setOllamaURL] = useState<string>("")
const { t } = useTranslation(["playground", "common"])
const [checkOllamaStatus] = useStorage("checkOllamaStatus", true)
const {
data: ollamaInfo,
status: ollamaStatus,
refetch,
isRefetching
} = useQuery({
queryKey: ["ollamaStatus"],
queryFn: async () => {
const ollamaURL = await getOllamaURL()
const isOk = await isOllamaRunning()
onSubmit,
setMessages,
setHistory,
setHistoryId,
historyId,
clearChat,
setSelectedModel,
temporaryChat,
setSelectedSystemPrompt
} = useMessageOption()
if (ollamaURL) {
saveOllamaURL(ollamaURL)
}
const queryClient = useQueryClient()
return {
isOk,
ollamaURL
}
const questions = [
{
title: "最近一年大型语言模型的技术进展有哪些?",
icon: <img src={RocketSvg} alt="Rocket" className="w-10 my-0" />,
},
enabled: checkOllamaStatus
{
title: "生成式AI在企业中有哪些具体应用场景",
icon: <img src={BulbSvg} alt="Rocket" className="w-10 my-0" />,
},
{
title: "多模态学习技术的最新研究方向是什么?",
icon: <img src={EyeSvg} alt="Rocket" className="w-10 my-0" />,
},
{
title: "当前AI芯片市场格局和未来三年发展趋势如何",
icon: <img src={ASvg} alt="Rocket" className="w-10 my-0" />,
},
{
title: "主流深度学习框架性能与易用性对比分析?",
icon: <img src={BSvg} alt="Rocket" className="w-10 my-0" />,
},
{
title: "国内外AI伦理治理框架有哪些最佳实践",
icon: <img src={CSvg} alt="Rocket" className="w-10 my-0" />,
},
{
title: "大规模知识图谱构建与应用最新进展?",
icon: <img src={DSvg} alt="Rocket" className="w-10 my-0" />,
},
{
title: "计算机视觉领域近期突破性技术有哪些?",
icon: <img src={ESvg} alt="Rocket" className="w-10 my-0" />,
},
{
title: "量子计算对AI算法的影响与应用前景",
icon: <img src={FSvg} alt="Rocket" className="w-10 my-0" />,
},
]
const { mutateAsync: sendMessage } = useMutation({
mutationFn: onSubmit,
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ["fetchChatHistory"]
})
}
})
useEffect(() => {
if (ollamaInfo?.ollamaURL) {
setOllamaURL(ollamaInfo.ollamaURL)
}
}, [ollamaInfo])
if (!checkOllamaStatus) {
return (
<div className="mx-auto sm:max-w-xl px-4 mt-10">
<div className="rounded-lg justify-center items-center flex flex-col border p-8 bg-gray-50 dark:bg-[#262626] dark:border-gray-600">
<h1 className="text-sm font-medium text-center text-gray-500 dark:text-gray-400 flex gap-3 items-center justify-center">
<span >👋</span>
<span className="text-gray-700 dark:text-gray-300">
{t("welcome")}
</span>
</h1>
</div>
</div>
)
function handleQuestion(message: string) {
void sendMessage({message, image: ''})
}
return (
<div className="mx-auto sm:max-w-xl px-4 mt-10">
<div className="rounded-lg justify-center items-center flex flex-col border p-8 bg-gray-50 dark:bg-[#262626] dark:border-gray-600">
{(ollamaStatus === "pending" || isRefetching) && (
<div className="inline-flex items-center space-x-2">
<div className="w-3 h-3 bg-blue-500 rounded-full animate-pulse"></div>
<p className="dark:text-gray-400 text-gray-900">
{t("ollamaState.searching")}
</p>
</div>
)}
{!isRefetching && ollamaStatus === "success" ? (
ollamaInfo.isOk ? (
<div className="inline-flex items-center space-x-2">
<div className="w-3 h-3 bg-green-500 rounded-full animate-pulse"></div>
<p className="dark:text-gray-400 text-gray-900">
{t("ollamaState.running")}
</p>
</div>
) : (
<div className="flex flex-col space-y-2 justify-center items-center">
<div className="inline-flex space-x-2">
<div className="w-3 h-3 bg-red-500 rounded-full animate-pulse"></div>
<p className="dark:text-gray-400 text-gray-900">
{t("ollamaState.notRunning")}
</p>
</div>
<input
className="bg-gray-100 dark:bg-[#262626] dark:text-gray-100 rounded-md px-4 py-2 mt-2 w-full"
type="url"
value={ollamaURL}
onChange={(e) => setOllamaURL(e.target.value)}
/>
<button
onClick={() => {
saveOllamaURL(ollamaURL)
refetch()
}}
className="inline-flex mt-4 items-center rounded-md border border-transparent bg-black px-2 py-2 text-sm font-medium leading-4 text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:bg-white dark:text-gray-800 dark:hover:bg-gray-100 dark:focus:ring-gray-500 dark:focus:ring-offset-gray-100 disabled:opacity-50 ">
<RotateCcw className="h-4 w-4 mr-3" />
{t("common:retry")}
</button>
{ollamaURL &&
cleanUrl(ollamaURL) !== "http://127.0.0.1:11434" && (
<p className="text-xs text-gray-500 dark:text-gray-400 mb-4 text-center">
<Trans
i18nKey="playground:ollamaState.connectionError"
components={{
anchor: (
<a
href="https://github.com/n4ze3m/page-assist/blob/main/docs/connection-issue.md"
target="__blank"
className="text-blue-600 dark:text-blue-400"></a>
)
}}
/>
</p>
)}
</div>
)
) : null}
<div className="w-full p-4">
{/* 标题区域 */}
<div className="mb-4">
<h2 className="text-xl font-bold text-gray-800" style={{lineHeight: '0'}}></h2>
<p className="text-sm text-gray-500"></p>
</div>
{/* 卡片网格布局 */}
<Row gutter={[16, 16]} className="w-full">
{questions.map((item, index) => (
<Col key={index} xs={24} sm={12} md={8}>
<Card
hoverable
style={{backgroundColor: "#f3f4f6"}}
className="border border-gray-200 rounded-lg shadow-sm hover:shadow-md transition-shadow duration-200 cursor-pointer"
onClick={() => handleQuestion(item.title)}
>
<div className="flex items-center">
<div className="text-blue-500 mr-2">{item.icon}</div>
<div className="font-medium text-sm text-gray-800">{item.title}</div>
</div>
</Card>
</Col>
))}
</Row>
</div>
)
}

View File

@@ -207,11 +207,11 @@ export const PlaygroundForm = ({ dropedFile }: Props) => {
}
return (
<div className="flex w-full flex-col items-center p-2 pt-1 pb-4">
<div className="flex w-full flex-col items-center p-2 px-5 pt-1 pb-4">
<div className="relative z-10 flex w-full flex-col items-center justify-center gap-2 text-base">
<div className="relative flex w-full flex-row justify-center gap-2 lg:w-4/5">
<div className="relative flex w-full flex-row justify-center gap-2 lg:w-5/5">
<div
className={` bg-neutral-50 dark:bg-[#262626] relative w-full max-w-[48rem] p-1 backdrop-blur-lg duration-100 border border-gray-300 rounded-xl dark:border-gray-600
className={` bg-neutral-50 dark:bg-[#262626] relative w-full max-w-[65rem] p-1 backdrop-blur-lg duration-100 border border-gray-300 rounded-xl dark:border-gray-600
${temporaryChat ? "!bg-gray-200 dark:!bg-black " : ""}
`}>
<div