refactor(layout): 重构布局组件并添加视频播放功能

-重写 Header 组件,使用新的 OptionLayoutContext 替代 HistoryContext
- 新增 VideoPlayer 组件,用于播放视频
- 更新 Playground 组件,集成新的侧边栏和视频播放功能
- 重构 Layout 组件,支持新的选项布局
- 更新相关路由和导出导入逻辑,以支持上述更改
This commit is contained in:
zhaoweijie
2025-08-25 19:40:02 +08:00
parent 6f386709e2
commit 635e792e22
13 changed files with 580 additions and 128 deletions

View File

@@ -2,6 +2,7 @@ import React from "react"
import { PlaygroundForm } from "./PlaygroundForm"
import { PlaygroundChat } from "./PlaygroundChat"
import { PlaygroundSidebar } from "./PlaygroundSidebar.tsx"
import { useMessageOption } from "@/hooks/useMessageOption"
import { webUIResumeLastChat } from "@/services/app"
@@ -15,7 +16,6 @@ import { getLastUsedChatSystemPrompt } from "@/services/model-settings"
import { useStoreChatModelSettings } from "@/store/model"
import { useSmartScroll } from "@/hooks/useSmartScroll"
import { ChevronDown } from "lucide-react"
import { PlaygroundHistory } from "@/components/Common/Playground/History.tsx"
import { PlaygroundIod } from "@/components/Option/Playground/PlaygroundIod.tsx"
export const Playground = () => {
@@ -139,7 +139,7 @@ export const Playground = () => {
className={`relative flex gap-3 h-full items-center ${
dropState === "dragging" ? "bg-gray-100 dark:bg-gray-800" : ""
} bg-white dark:bg-[#171717]`}>
<PlaygroundHistory />
<PlaygroundSidebar />
<div className="h-full flex-1 overflow-x-hidden prose-lg flex flex-col items-center [&>*]:max-w-[848px] pt-[60px]">
<div
ref={containerRef}

View File

@@ -48,9 +48,6 @@ const PlaygroundIodProvider: React.FC<{ children: React.ReactNode }> = ({
const [detailMain, setDetailMain] = useState(<></>)
const currentIodMessage = useMemo<AllIodRegistryEntry | undefined>(() => {
console.log('messages', messages)
console.log("currentMessageId", currentMessageId)
console.log("iodLoading", iodLoading)
// loading 返回 undefined是为了避免数据不足三个的情况
if (iodLoading || !messages.length) {
return undefined
@@ -66,7 +63,6 @@ const PlaygroundIodProvider: React.FC<{ children: React.ReactNode }> = ({
const currentMessage = messages?.find(
(message) => message.id === currentMessageId
)
console.log("currentMessage", currentMessage)
return currentMessage?.iodSearch ? currentMessage.iodSources : undefined
}, [currentMessageId, messages, iodLoading])

View File

@@ -0,0 +1,277 @@
import { Sidebar } from "@/components/Option/Sidebar.tsx"
import React, { useMemo } from "react"
import { useMessageOption } from "@/hooks/useMessageOption.tsx"
import { useStoreChatModelSettings } from "@/store/model.tsx"
import {
Button,
Card,
Divider,
Menu,
MenuProps,
Popover,
Select,
Tooltip
} from "antd"
import { PageAssitDatabase } from "@/db"
import { EraserIcon, PanelLeftIcon } from "lucide-react"
import { useTranslation } from "react-i18next"
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { useOptionLayoutContext } from "@/components/Layouts/Layout.tsx"
import { PlusOutlined, RightOutlined } from "@ant-design/icons"
import { qaPrompt } from "@/libs/playground.tsx"
import { ProviderIcons } from "@/components/Common/ProviderIcon.tsx"
import { fetchChatModels } from "@/services/ollama.ts"
import logo from "@/assets/logo.png"
const ModelIcon = () => {
return (
<svg
className="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="9426"
width="16"
height="16">
<path
d="M509.952 161.512727c148.945455-82.850909 300.730182-91.229091 371.479273-20.945454s62.324364 221.509818-20.526546 370.501818h-0.465454a429.335273 429.335273 0 0 1 65.163636 284.392727 168.727273 168.727273 0 0 1-44.683636 85.643637 173.754182 173.754182 0 0 1-86.109091 44.683636 435.665455 435.665455 0 0 1-285.277091-65.675636 430.731636 430.731636 0 0 1-282.530909 63.813818 172.218182 172.218182 0 0 1-86.109091-44.683637c-70.283636-69.818182-62.370909-220.206545 19.502545-368.174545-81.966545-148.48-89.786182-298.309818-19.502545-368.686546s220.625455-62.324364 369.058909 19.130182z m291.886545 440.785455a901.818182 901.818182 0 0 1-92.16 106.589091 934.027636 934.027636 0 0 1-108.916363 93.602909 586.891636 586.891636 0 0 0 58.600727 21.410909c74.938182 22.341818 127.069091 19.502545 155.508364-8.843636l-0.465455 0.884363c28.811636-28.392727 31.697455-80.523636 8.843637-155.508363a546.443636 546.443636 0 0 0-21.41091-58.135273z m-582.74909-0.465455a539.927273 539.927273 0 0 0-20.433455 55.854546c-22.295273 75.357091-19.549091 127.022545 8.797091 155.368727s80.151273 31.697455 155.508364 8.936727h-0.558546a539.927273 539.927273 0 0 0 55.854546-20.526545 967.400727 967.400727 0 0 1-199.214546-199.726546z m290.90909-332.753454a851.781818 851.781818 0 0 0-131.258181 108.404363 823.296 823.296 0 0 0-109.847273 133.12 823.854545 823.854545 0 0 0 109.847273 133.12v-0.884363a852.293818 852.293818 0 0 0 131.211636 108.357818 846.754909 846.754909 0 0 0 133.538909-109.800727 856.436364 856.436364 0 0 0 108.962909-131.258182 852.852364 852.852364 0 0 0-108.962909-131.211637 829.998545 829.998545 0 0 0-133.538909-109.847272zM503.994182 418.909091a94.347636 94.347636 0 1 1-35.84 10.705454 92.811636 92.811636 0 0 1 35.84-10.705454z m310.877091-212.340364c-28.253091-28.299636-80.151273-31.557818-155.508364-8.750545a591.592727 591.592727 0 0 0-58.600727 21.876363 933.794909 933.794909 0 0 1 108.869818 93.556364 947.060364 947.060364 0 0 1 92.718545 107.054546 545.326545 545.326545 0 0 0 21.41091-58.181819q33.559273-113.058909-8.843637-155.508363zM363.054545 199.68c-74.938182-22.295273-127.069091-19.549091-155.508363 8.843636v-0.465454c-28.997818 28.392727-31.744 80.523636-8.936727 155.508363a507.345455 507.345455 0 0 0 20.433454 56.273455A976.663273 976.663273 0 0 1 418.909091 220.206545a541.230545 541.230545 0 0 0-55.854546-20.526545z m0 0"
fill="#696F85"
p-id="9427"></path>
</svg>
)
}
export const PlaygroundSidebar = () => {
const { setSystemPrompt } = useStoreChatModelSettings()
const { showOptionSidebar, setShowOptionSidebar, setShowVideo } = useOptionLayoutContext()
const {
setMessages,
setHistory,
setHistoryId,
historyId,
clearChat,
selectedModel,
setSelectedModel,
temporaryChat,
setSelectedSystemPrompt,
stopStreamingRequest
} = useMessageOption()
const { t } = useTranslation(["option", "common", "settings"])
const queryClient = useQueryClient()
type MenuItem = Required<MenuProps>["items"][number]
const qaPromptItems = useMemo<MenuItem[]>(() => {
return [
{
key: "qaPrompt",
label: "热点问题",
type: "group" as const,
children: qaPrompt.map((item) => {
return {
key: item.id,
label: (
<div className="flex items-center gap-2 truncate w-full">
<p className="w-5 h-5 [&_.ant-avatar]:!w-full [&_.ant-avatar]:!h-full [&_.ant-avatar]:relative [&_.ant-avatar]:-top-3">
{item.icon}
</p>
<span className="flex-1 truncate" title={item.title}>
{item.title}
</span>
</div>
)
}
})
}
]
}, [])
const { onSubmit } = useMessageOption()
const { mutateAsync: sendMessage } = useMutation({
mutationFn: onSubmit,
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ["fetchChatHistory"]
})
}
})
const onClickQaPromptItem: MenuProps["onClick"] = (e) => {
const record = qaPrompt.find((item) => item.id === e.key)
void sendMessage({ message: record.title, image: "" })
}
// 大模型
const { data: models, isLoading: isModelsLoading } = useQuery({
queryKey: ["fetchModel"],
queryFn: () => fetchChatModels({ returnEmpty: true }),
refetchIntervalInBackground: false,
placeholderData: (prev) => prev
})
// 是否隐藏logo
const hideLogo = useMemo(() => {
return localStorage.getItem("hideLogo") === "true"
}, [])
return (
<Card
className={`flex flex-col [&_.ant-card-body]:h-full w-[300px] overflow-hidden h-full pb-5 transition-all duration-300 ease-in-out backdrop-blur-lg !bg-[#f3f4f6]`}
style={{ width: showOptionSidebar ? "300px" : "0" }}>
{/*Header*/}
<div className="flex flex-col overflow-y-hidden h-full">
<div className="flex items-center justify-between transition-all duration-300 ease-in-out w-[250px]">
<div className="flex items-center gap-2 cursor-pointer" onClick={() => setShowVideo(true)}>
{!hideLogo && <img src={logo} alt="logo" className="w-8" />}
<h2 className="text-xl font-bold text-zinc-700 dark:text-zinc-300 mr-3">
<span className="text-[#d30100]"></span>
</h2>
</div>
<button
className="text-gray-500 dark:text-gray-400"
onClick={() => {
setShowOptionSidebar(!showOptionSidebar)
}}>
<PanelLeftIcon className="w-6 h-6" />
</button>
</div>
<div className="flex flex-col gap-1">
{/*新建对话*/}
<Button
color="purple"
variant="filled"
size="large"
className="w-full mt-4 hover:!bg-[#0057ff1a]"
style={{
color: "#0057ff",
background: "#0057ff0f",
border: "1px solid #0066ff26"
}}
onClick={clearChat}>
<div className="flex items-center justify-between w-full">
<div className="flex items-center">
<PlusOutlined
className="text-sm"
style={{ fontSize: "16px", fontWeight: 500 }}
/>
<span className="font-medium ml-2.5">{t("newChat")}</span>
</div>
</div>
</Button>
{/*选择智能体*/}
<Popover
placement="right"
content={
<Select
className="w-80"
placeholder={t("common:selectAModel")}
// loadingText={t("common:selectAModel")}
value={selectedModel}
onChange={(e) => {
setSelectedModel(e)
localStorage.setItem("selectedModel", e)
}}
filterOption={(input, option) => {
//@ts-ignore
return (
option?.label?.props["data-title"]
?.toLowerCase()
?.indexOf(input.toLowerCase()) >= 0
)
}}
showSearch
loading={isModelsLoading}
options={models?.map((model) => ({
label: (
<span
key={model.model}
data-title={model.name}
className="flex flex-row gap-3 items-center ">
<ProviderIcons
provider={model?.provider}
className="w-5 h-5"
/>
<span className="line-clamp-2">{model.name}</span>
</span>
),
value: model.model
}))}
size="large"
// onRefresh={() => {
// refetch()
// }}
/>
}>
<Button
size="large"
color="default"
variant="text"
className="w-full !justify-between !text-[#000000d9] font-normal">
<div className="flex items-center gap-2.5">
<ModelIcon />
<span className="!text-[#000000d9] font-normal text-sm">
</span>
</div>
<RightOutlined style={{ color: "#0000004d" }} />
</Button>
</Popover>
<Divider size="small" />
{/*热门搜索*/}
<Menu
items={qaPromptItems}
onClick={onClickQaPromptItem}
className="!bg-[#f3f4f6] !border-r-0"
/>
</div>
<Divider size="small" />
<div className="pb-1.5 pl-4 text-sm text-[#00000073] flex items-center justify-between pr-2">
<span></span>
<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 flex-1 pl-7">
<Sidebar
onClose={() => setShowOptionSidebar(true)}
setMessages={setMessages}
setHistory={setHistory}
setHistoryId={setHistoryId}
setSelectedModel={setSelectedModel}
setSelectedSystemPrompt={setSelectedSystemPrompt}
clearChat={clearChat}
historyId={historyId}
setSystemPrompt={setSystemPrompt}
temporaryChat={temporaryChat}
stopStreamingRequest={stopStreamingRequest}
history={history}
/>
</div>
</div>
</Card>
)
}