refactor(layout): 重构布局组件并添加视频播放功能
-重写 Header 组件,使用新的 OptionLayoutContext 替代 HistoryContext - 新增 VideoPlayer 组件,用于播放视频 - 更新 Playground 组件,集成新的侧边栏和视频播放功能 - 重构 Layout 组件,支持新的选项布局 - 更新相关路由和导出导入逻辑,以支持上述更改
This commit is contained in:
parent
6f386709e2
commit
635e792e22
BIN
src/assets/video.mp4
Normal file
BIN
src/assets/video.mp4
Normal file
Binary file not shown.
@ -1,7 +1,8 @@
|
||||
import { useForm } from "@mantine/form"
|
||||
import React from "react"
|
||||
import React, { useEffect, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import useDynamicTextareaSize from "~/hooks/useDynamicTextareaSize"
|
||||
import TextArea from "antd/es/input/TextArea"
|
||||
|
||||
type Props = {
|
||||
value: string
|
||||
@ -14,6 +15,14 @@ export const EditMessageForm = (props: Props) => {
|
||||
const [isComposing, setIsComposing] = React.useState(false)
|
||||
const textareaRef = React.useRef<HTMLTextAreaElement>(null)
|
||||
const { t } = useTranslation("common")
|
||||
const [value, setValue] = useState(props.value);
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
setValue(props.value)
|
||||
},
|
||||
[props.value]
|
||||
);
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
@ -29,46 +38,27 @@ export const EditMessageForm = (props: Props) => {
|
||||
return (
|
||||
<form
|
||||
onSubmit={form.onSubmit((data) => {
|
||||
if (isComposing) return
|
||||
props.onClose()
|
||||
props.onSumbit(data.message, true)
|
||||
props.onSumbit(value, true)
|
||||
})}
|
||||
className="flex flex-col gap-2">
|
||||
<textarea
|
||||
{...form.getInputProps("message")}
|
||||
onCompositionStart={() => setIsComposing(true)}
|
||||
onCompositionEnd={() => setIsComposing(false)}
|
||||
className="flex flex-col gap-2 w-96 ml-auto">
|
||||
<TextArea
|
||||
required
|
||||
rows={1}
|
||||
rows={2}
|
||||
value={value}
|
||||
style={{ minHeight: "60px" }}
|
||||
tabIndex={0}
|
||||
onChange={(e) => {
|
||||
setValue(e.target.value)
|
||||
}}
|
||||
placeholder={t("editMessage.placeholder")}
|
||||
ref={textareaRef}
|
||||
className="w-full bg-transparent focus-within:outline-none focus:ring-0 focus-visible:ring-0 ring-0 dark:ring-0 border-0 dark:text-gray-100"
|
||||
/>
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
<div
|
||||
className={`w-full flex ${
|
||||
!props.isBot ? "justify-between" : "justify-end"
|
||||
!props.isBot ? "justify-end" : "justify-end"
|
||||
}`}>
|
||||
{!props.isBot && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
props.onSumbit(form.values.message, false)
|
||||
props.onClose()
|
||||
}}
|
||||
aria-label={t("save")}
|
||||
className="border border-gray-600 px-2 py-1.5 rounded-lg text-gray-700 dark:text-gray-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-gray-500 hover:bg-gray-100 dark:hover:bg-gray-900 text-sm">
|
||||
{t("save")}
|
||||
</button>
|
||||
)}
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
aria-label={t("save")}
|
||||
className="bg-black px-2 py-1.5 rounded-lg text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-gray-500 hover:bg-gray-900 text-sm">
|
||||
{props.isBot ? t("save") : t("saveAndSubmit")}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={props.onClose}
|
||||
@ -76,6 +66,12 @@ export const EditMessageForm = (props: Props) => {
|
||||
className="border dark:border-gray-600 px-2 py-1.5 rounded-lg text-gray-700 dark:text-gray-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-gray-500 hover:bg-gray-100 dark:hover:bg-gray-900 text-sm">
|
||||
{t("cancel")}
|
||||
</button>
|
||||
|
||||
<button
|
||||
aria-label={t("save")}
|
||||
className="bg-[#0057ff] px-2 py-1.5 rounded-lg text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-gray-500 text-sm">
|
||||
{props.isBot ? t("save") : t("saveAndSubmit")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>{" "}
|
||||
|
@ -275,7 +275,7 @@ export const PlaygroundIodRelevant: React.FC<Props> = ({ className }) => {
|
||||
/>
|
||||
个{" "}
|
||||
</span>
|
||||
数据集
|
||||
数据集,引用 3个 数据集作为参考
|
||||
</p>
|
||||
) : (
|
||||
""
|
||||
@ -314,7 +314,7 @@ export const PlaygroundIodRelevant: React.FC<Props> = ({ className }) => {
|
||||
/>
|
||||
个{" "}
|
||||
</span>
|
||||
场景
|
||||
场景,引用 3个 场景作为参考
|
||||
</p>
|
||||
) : (
|
||||
""
|
||||
@ -358,7 +358,7 @@ export const PlaygroundIodRelevant: React.FC<Props> = ({ className }) => {
|
||||
/>
|
||||
个{" "}
|
||||
</span>
|
||||
组织
|
||||
组织,引用 3个 组织作为参考
|
||||
</p>
|
||||
) : (
|
||||
""
|
||||
|
@ -82,7 +82,7 @@ export const PlaygroundMessage: React.FC<Props> = (props) => {
|
||||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
<div className={`flex flex-grow flex-col`}>
|
||||
<div className={`flex flex-grow flex-col w-full`}>
|
||||
{!editMode ? (
|
||||
props.isBot ? (
|
||||
<>
|
||||
@ -239,7 +239,7 @@ export const PlaygroundMessage: React.FC<Props> = (props) => {
|
||||
// : "flex"
|
||||
}`}>
|
||||
{props.isTTSEnabled && (
|
||||
<Tooltip title={t("tts")}>
|
||||
<Tooltip title={t("tts")} className="hidden">
|
||||
<button
|
||||
aria-label={t("tts")}
|
||||
onClick={() => {
|
||||
@ -297,6 +297,7 @@ export const PlaygroundMessage: React.FC<Props> = (props) => {
|
||||
|
||||
{props.generationInfo && (
|
||||
<Popover
|
||||
className="hidden"
|
||||
content={
|
||||
<GenerationInfo generationInfo={props.generationInfo} />
|
||||
}
|
||||
@ -322,8 +323,8 @@ export const PlaygroundMessage: React.FC<Props> = (props) => {
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!props.hideEditAndRegenerate && (
|
||||
<Tooltip title={t("edit")}>
|
||||
{(!props.hideEditAndRegenerate && !props.isBot) && (
|
||||
<Tooltip title={t("edit")} className="hidden">
|
||||
<button
|
||||
onClick={() => setEditMode(true)}
|
||||
aria-label={t("edit")}
|
||||
@ -333,7 +334,7 @@ export const PlaygroundMessage: React.FC<Props> = (props) => {
|
||||
</Tooltip>
|
||||
)}
|
||||
{
|
||||
<Tooltip title="收藏">
|
||||
<Tooltip title="收藏" className="hidden">
|
||||
<button
|
||||
aria-label="收藏"
|
||||
className="flex items-center justify-center w-6 h-6 rounded-full bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500">
|
||||
@ -342,7 +343,7 @@ export const PlaygroundMessage: React.FC<Props> = (props) => {
|
||||
</Tooltip>
|
||||
}
|
||||
{
|
||||
<Tooltip title="发布语用">
|
||||
<Tooltip title="发布语用" className="hidden">
|
||||
<button
|
||||
aria-label="发布语用"
|
||||
className="flex items-center justify-center w-6 h-6 rounded-full bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500">
|
||||
@ -351,7 +352,7 @@ export const PlaygroundMessage: React.FC<Props> = (props) => {
|
||||
</Tooltip>
|
||||
}
|
||||
{
|
||||
<Tooltip title="发布对话">
|
||||
<Tooltip title="发布对话" className="hidden">
|
||||
<button
|
||||
aria-label="发布对话"
|
||||
className="flex items-center justify-center w-6 h-6 rounded-full bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500">
|
||||
@ -360,7 +361,7 @@ export const PlaygroundMessage: React.FC<Props> = (props) => {
|
||||
</Tooltip>
|
||||
}
|
||||
{
|
||||
<Tooltip title="点赞">
|
||||
<Tooltip title="点赞" className="hidden">
|
||||
<button
|
||||
aria-label="点赞"
|
||||
className="flex items-center justify-center w-6 h-6 rounded-full bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500">
|
||||
@ -369,7 +370,7 @@ export const PlaygroundMessage: React.FC<Props> = (props) => {
|
||||
</Tooltip>
|
||||
}
|
||||
{
|
||||
<Tooltip title="点踩">
|
||||
<Tooltip title="点踩" className="hidden">
|
||||
<button
|
||||
aria-label="点踩"
|
||||
className="flex items-center justify-center w-6 h-6 rounded-full bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500">
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { useContext, useMemo, useState } from "react"
|
||||
import { HistoryContext } from "@/components/Layouts/Layout.tsx"
|
||||
import React, { useMemo, useState } from "react"
|
||||
import { useOptionLayoutContext } from "@/components/Layouts/Layout.tsx"
|
||||
import { PanelLeftIcon } from "lucide-react"
|
||||
import { Button, Tooltip } from "antd"
|
||||
import { PlusOutlined } from "@ant-design/icons"
|
||||
@ -13,22 +13,20 @@ import { NotCollectIcon } from "@/components/Icons/NotCollect.tsx"
|
||||
import { CollectIcon } from "@/components/Icons/Collect.tsx"
|
||||
import { SettingIcon } from "@/components/Icons/Setting.tsx"
|
||||
|
||||
type Props = {
|
||||
setOpenModelSettings: (open: boolean) => void
|
||||
}
|
||||
type Props = {}
|
||||
|
||||
export const Header: React.FC<Props> = ({ setOpenModelSettings }) => {
|
||||
export const Header: React.FC<Props> = ({}) => {
|
||||
const location = useLocation()
|
||||
|
||||
const { show, setShow } = useContext(HistoryContext)
|
||||
const { showOptionSidebar, setShowOptionSidebar } = useOptionLayoutContext()
|
||||
|
||||
const showLeft = useMemo<boolean>(() => {
|
||||
console.log(location.pathname)
|
||||
if (location.pathname.includes("/settings")) {
|
||||
return true
|
||||
}
|
||||
return show
|
||||
}, [location.pathname, show])
|
||||
return showOptionSidebar
|
||||
}, [location.pathname, showOptionSidebar])
|
||||
|
||||
const { t } = useTranslation(["option", "common", "settings"])
|
||||
|
||||
@ -42,14 +40,14 @@ export const Header: React.FC<Props> = ({ setOpenModelSettings }) => {
|
||||
const [collect, setCollect] = useState<boolean>(false)
|
||||
return (
|
||||
<div
|
||||
className={`h-[60px] absolute inset-0 pl-5 z-10 flex items-center transition-all duration-300 ease-in-out ${show && !location.pathname.includes("/settings") ? "left-[300px]" : ""}`}>
|
||||
className={`h-[60px] absolute inset-0 pl-5 z-10 flex items-center transition-all duration-300 ease-in-out ${showOptionSidebar && !location.pathname.includes("/settings") ? "left-[300px]" : ""}`}>
|
||||
{/*控制侧边栏显示隐藏与新建对话*/}
|
||||
{!showLeft && (
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
className="text-gray-500 dark:text-gray-400"
|
||||
onClick={() => {
|
||||
setShow(!show)
|
||||
setShowOptionSidebar(!showOptionSidebar)
|
||||
}}>
|
||||
<PanelLeftIcon className="w-6 h-6" />
|
||||
</button>
|
||||
@ -94,7 +92,7 @@ export const Header: React.FC<Props> = ({ setOpenModelSettings }) => {
|
||||
w-[600px] h-[60px] dark:bg-black
|
||||
flex items-center justify-center
|
||||
transition-[top] drop-shadow
|
||||
${show ? "-top-[60px]" : "-top-[2px] delay-200"}
|
||||
${showOptionSidebar ? "-top-[60px]" : "-top-[2px] delay-200"}
|
||||
`}>
|
||||
<svg
|
||||
className="icon"
|
||||
|
@ -1,47 +1,86 @@
|
||||
import React, { useCallback, useEffect, useState } from "react"
|
||||
import React, { useContext, useState } from "react"
|
||||
|
||||
import { CurrentChatModelSettings } from "../Common/Settings/CurrentChatModelSettings"
|
||||
import { Header } from "./Header.tsx"
|
||||
import IodVideo from "@/components/Option/VideoPlayer"
|
||||
|
||||
interface History {
|
||||
show: boolean
|
||||
setShow: (show: boolean) => void
|
||||
interface OptionLayoutContextType {
|
||||
showOptionSidebar: boolean
|
||||
setShowOptionSidebar: (show: boolean) => void
|
||||
showVideo: boolean
|
||||
setShowVideo: (show: boolean) => void
|
||||
}
|
||||
|
||||
export const HistoryContext = React.createContext<History>({
|
||||
show: true,
|
||||
setShow: () => {}
|
||||
const OptionLayoutContext = React.createContext<OptionLayoutContextType>({
|
||||
showOptionSidebar: true,
|
||||
setShowOptionSidebar: () => {},
|
||||
showVideo: true,
|
||||
setShowVideo: () => {}
|
||||
})
|
||||
|
||||
// 创建自定义 hook 以便子组件使用
|
||||
export const useOptionLayoutContext = () => {
|
||||
const context = useContext(OptionLayoutContext)
|
||||
if (context === undefined) {
|
||||
throw new Error(
|
||||
"useOptionLayoutContext must be used within a OptionLayoutProvider"
|
||||
)
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
const OptionLayoutProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [showHistory, setShowHistory] = useState(true)
|
||||
|
||||
const [showVideo, setShowVideo] = useState<boolean>(false)
|
||||
|
||||
return (
|
||||
<OptionLayoutContext.Provider
|
||||
value={{
|
||||
showOptionSidebar: showHistory,
|
||||
setShowOptionSidebar: setShowHistory,
|
||||
showVideo,
|
||||
setShowVideo
|
||||
}}>
|
||||
{children}
|
||||
</OptionLayoutContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const OptionLayoutMain: React.FC<{ children: React.ReactNode }> = ({
|
||||
children
|
||||
}) => {
|
||||
const { showVideo } = useOptionLayoutContext()
|
||||
|
||||
if (showVideo) {
|
||||
return <IodVideo />
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default function OptionLayout({
|
||||
children
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const [showHistory, setShowHistory] = useState(true)
|
||||
const [openModelSettings, setOpenModelSettings] = useState(false)
|
||||
|
||||
const historyContextValue = {
|
||||
show: showHistory,
|
||||
setShow: setShowHistory
|
||||
}
|
||||
|
||||
const useToggle = useCallback(() => {
|
||||
setShowHistory(!showHistory)
|
||||
}, [showHistory])
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full">
|
||||
<main className="relative h-dvh w-full">
|
||||
{/*<div className="relative z-10 w-full">*/}
|
||||
{/*</div>*/}
|
||||
{/* <div className="relative flex h-full flex-col items-center"> */}
|
||||
<HistoryContext.Provider value={historyContextValue}>
|
||||
<Header setOpenModelSettings={setOpenModelSettings} />
|
||||
{children}
|
||||
</HistoryContext.Provider>
|
||||
<OptionLayoutProvider>
|
||||
<OptionLayoutMain>{children}</OptionLayoutMain>
|
||||
</OptionLayoutProvider>
|
||||
{/* </div> */}
|
||||
|
||||
<CurrentChatModelSettings
|
||||
open={openModelSettings}
|
||||
setOpen={setOpenModelSettings}
|
||||
|
@ -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}
|
||||
|
@ -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])
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Sidebar } from "@/components/Option/Sidebar.tsx"
|
||||
import React, { useContext, useMemo } from "react"
|
||||
import React, { useMemo } from "react"
|
||||
import { useMessageOption } from "@/hooks/useMessageOption.tsx"
|
||||
import { useStoreChatModelSettings } from "@/store/model.tsx"
|
||||
import {
|
||||
@ -16,7 +16,7 @@ import { PageAssitDatabase } from "@/db"
|
||||
import { EraserIcon, PanelLeftIcon } from "lucide-react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||
import { HistoryContext } from "@/components/Layouts/Layout.tsx"
|
||||
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"
|
||||
@ -41,10 +41,10 @@ const ModelIcon = () => {
|
||||
)
|
||||
}
|
||||
|
||||
export const PlaygroundHistory = () => {
|
||||
export const PlaygroundSidebar = () => {
|
||||
const { setSystemPrompt } = useStoreChatModelSettings()
|
||||
|
||||
const { show, setShow } = useContext(HistoryContext)
|
||||
const { showOptionSidebar, setShowOptionSidebar, setShowVideo } = useOptionLayoutContext()
|
||||
|
||||
const {
|
||||
setMessages,
|
||||
@ -55,7 +55,8 @@ export const PlaygroundHistory = () => {
|
||||
selectedModel,
|
||||
setSelectedModel,
|
||||
temporaryChat,
|
||||
setSelectedSystemPrompt
|
||||
setSelectedSystemPrompt,
|
||||
stopStreamingRequest
|
||||
} = useMessageOption()
|
||||
|
||||
const { t } = useTranslation(["option", "common", "settings"])
|
||||
@ -77,7 +78,9 @@ export const PlaygroundHistory = () => {
|
||||
<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>
|
||||
<span className="flex-1 truncate" title={item.title}>
|
||||
{item.title}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -118,19 +121,21 @@ export const PlaygroundHistory = () => {
|
||||
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: show ? "300px" : "0" }}>
|
||||
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={() => {
|
||||
setShow(!show)
|
||||
setShowOptionSidebar(!showOptionSidebar)
|
||||
}}>
|
||||
<PanelLeftIcon className="w-6 h-6" />
|
||||
</button>
|
||||
@ -252,7 +257,7 @@ export const PlaygroundHistory = () => {
|
||||
</div>
|
||||
<div className="overflow-y-auto flex-1 pl-7">
|
||||
<Sidebar
|
||||
onClose={() => setShow(true)}
|
||||
onClose={() => setShowOptionSidebar(true)}
|
||||
setMessages={setMessages}
|
||||
setHistory={setHistory}
|
||||
setHistoryId={setHistoryId}
|
||||
@ -262,6 +267,7 @@ export const PlaygroundHistory = () => {
|
||||
historyId={historyId}
|
||||
setSystemPrompt={setSystemPrompt}
|
||||
temporaryChat={temporaryChat}
|
||||
stopStreamingRequest={stopStreamingRequest}
|
||||
history={history}
|
||||
/>
|
||||
</div>
|
@ -33,6 +33,7 @@ type Props = {
|
||||
setSelectedModel: (model: string) => void
|
||||
setSelectedSystemPrompt: (prompt: string) => void
|
||||
setSystemPrompt: (prompt: string) => void
|
||||
stopStreamingRequest: () => void
|
||||
clearChat: () => void
|
||||
temporaryChat: boolean
|
||||
historyId: string
|
||||
@ -46,6 +47,7 @@ export const Sidebar = ({
|
||||
setHistoryId,
|
||||
setSelectedModel,
|
||||
setSelectedSystemPrompt,
|
||||
stopStreamingRequest,
|
||||
clearChat,
|
||||
historyId,
|
||||
setSystemPrompt,
|
||||
@ -140,6 +142,40 @@ export const Sidebar = ({
|
||||
}
|
||||
})
|
||||
|
||||
const handleHistoryClick = async (chat: any) => {
|
||||
const db = new PageAssitDatabase()
|
||||
const history = await db.getChatHistory(chat.id)
|
||||
setHistoryId(chat.id)
|
||||
setHistory(formatToChatHistory(history))
|
||||
setMessages(formatToMessage(history))
|
||||
stopStreamingRequest()
|
||||
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()
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`overflow-y-auto z-99 ${temporaryChat ? "pointer-events-none opacity-50" : ""}`}>
|
||||
@ -181,38 +217,7 @@ export const Sidebar = ({
|
||||
)}
|
||||
<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()
|
||||
}}>
|
||||
onClick={() => handleHistoryClick(chat)}>
|
||||
<span className="flex-grow truncate">{chat.title}</span>
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
|
410
src/components/Option/VideoPlayer/index.tsx
Normal file
410
src/components/Option/VideoPlayer/index.tsx
Normal file
@ -0,0 +1,410 @@
|
||||
import React, { useEffect, useRef, useState } from "react"
|
||||
import iodVideo from "@/assets/video.mp4"
|
||||
import { useOptionLayoutContext } from "@/components/Layouts/Layout.tsx"
|
||||
import {
|
||||
ExpandOutlined,
|
||||
PauseCircleOutlined,
|
||||
PlayCircleOutlined
|
||||
} from "@ant-design/icons"
|
||||
import logo from "@/assets/logo.png"
|
||||
|
||||
const VideoPlayer = () => {
|
||||
const { setShowVideo } = useOptionLayoutContext()
|
||||
const videoRef = useRef<HTMLVideoElement>(null)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const controlsTimerRef = useRef<NodeJS.Timeout | null>(null)
|
||||
const mouseMoveTimerRef = useRef<NodeJS.Timeout | null>(null)
|
||||
const isPlayingRef = useRef(false)
|
||||
|
||||
const [isPlaying, setIsPlaying] = useState(false)
|
||||
const [currentTime, setCurrentTime] = useState(0)
|
||||
const [duration, setDuration] = useState(0)
|
||||
const [volume, setVolume] = useState(1)
|
||||
const [isMuted, setIsMuted] = useState(false)
|
||||
const [showControls, setShowControls] = useState(false)
|
||||
const [isBuffering, setIsBuffering] = useState(false)
|
||||
|
||||
// 更新 isPlayingRef 当状态变化时
|
||||
useEffect(() => {
|
||||
isPlayingRef.current = isPlaying
|
||||
}, [isPlaying])
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (seconds: number) => {
|
||||
if (isNaN(seconds)) return "00:00"
|
||||
const minutes = Math.floor(seconds / 60)
|
||||
const secs = Math.floor(seconds % 60)
|
||||
return `${minutes.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`
|
||||
}
|
||||
|
||||
// 处理播放/暂停
|
||||
const togglePlayPause = () => {
|
||||
const video = videoRef.current
|
||||
if (!video) return
|
||||
|
||||
if (isPlaying) {
|
||||
video.pause()
|
||||
setIsPlaying(false)
|
||||
} else {
|
||||
video.play().catch((error) => {
|
||||
console.error("播放失败:", error)
|
||||
})
|
||||
setIsPlaying(true)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理音量变化
|
||||
const handleVolumeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newVolume = parseFloat(e.target.value)
|
||||
setVolume(newVolume)
|
||||
if (videoRef.current) {
|
||||
videoRef.current.volume = newVolume
|
||||
setIsMuted(newVolume === 0)
|
||||
}
|
||||
}
|
||||
|
||||
// 切换静音
|
||||
const toggleMute = () => {
|
||||
const newMuted = !isMuted
|
||||
setIsMuted(newMuted)
|
||||
if (videoRef.current) {
|
||||
videoRef.current.muted = newMuted
|
||||
}
|
||||
}
|
||||
|
||||
// 进度条点击处理
|
||||
const handleProgressClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
const progressBar = e.currentTarget
|
||||
const clickPosition = e.nativeEvent.offsetX
|
||||
const progressBarWidth = progressBar.offsetWidth
|
||||
if (duration > 0 && videoRef.current) {
|
||||
const newTime = (clickPosition / progressBarWidth) * duration
|
||||
videoRef.current.currentTime = newTime
|
||||
}
|
||||
}
|
||||
|
||||
// 全屏切换
|
||||
const toggleFullscreen = () => {
|
||||
const videoContainer = containerRef.current
|
||||
if (!videoContainer) return
|
||||
|
||||
if (!document.fullscreenElement) {
|
||||
if (videoContainer.requestFullscreen) {
|
||||
videoContainer.requestFullscreen().catch((err) => {
|
||||
console.error("全屏切换失败:", err)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if (document.exitFullscreen) {
|
||||
document.exitFullscreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleEnded = () => {
|
||||
setIsPlaying(false)
|
||||
setShowVideo(false)
|
||||
}
|
||||
|
||||
// 控制栏显示/隐藏 - 与原始HTML版本行为完全一致,添加防抖功能
|
||||
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
// 清除之前的防抖定时器
|
||||
if (mouseMoveTimerRef.current) {
|
||||
clearTimeout(mouseMoveTimerRef.current)
|
||||
}
|
||||
|
||||
// 设置新的防抖定时器
|
||||
mouseMoveTimerRef.current = setTimeout(() => {
|
||||
const container = containerRef.current
|
||||
if (!container) return
|
||||
|
||||
const containerHeight = container.offsetHeight
|
||||
const mouseY = e.clientY - container.getBoundingClientRect().top
|
||||
|
||||
// 如果鼠标在底部150px区域内
|
||||
if (mouseY > containerHeight - 150) {
|
||||
// 清除之前的隐藏定时器
|
||||
if (controlsTimerRef.current) {
|
||||
clearTimeout(controlsTimerRef.current)
|
||||
}
|
||||
// 立即显示控制器
|
||||
setShowControls(true)
|
||||
} else {
|
||||
// 鼠标离开底部区域,设置定时器隐藏控制器
|
||||
if (controlsTimerRef.current) {
|
||||
clearTimeout(controlsTimerRef.current)
|
||||
}
|
||||
controlsTimerRef.current = setTimeout(() => {
|
||||
setShowControls(false)
|
||||
}, 300) // 300ms后隐藏
|
||||
}
|
||||
}, 10) // 10ms 防抖延迟
|
||||
}
|
||||
|
||||
// 鼠标离开整个视频容器时立即隐藏控制器
|
||||
const handleMouseLeave = () => {
|
||||
// 清除防抖定时器
|
||||
if (mouseMoveTimerRef.current) {
|
||||
clearTimeout(mouseMoveTimerRef.current)
|
||||
}
|
||||
|
||||
// 清除控制栏隐藏定时器
|
||||
if (controlsTimerRef.current) {
|
||||
clearTimeout(controlsTimerRef.current)
|
||||
}
|
||||
|
||||
// 立即隐藏控制栏
|
||||
setShowControls(false)
|
||||
}
|
||||
|
||||
// 视频事件处理
|
||||
useEffect(() => {
|
||||
const video = videoRef.current
|
||||
if (!video) return
|
||||
|
||||
const handleLoadedMetadata = () => {
|
||||
setDuration(video.duration)
|
||||
}
|
||||
|
||||
const handleTimeUpdate = () => {
|
||||
setCurrentTime(video.currentTime)
|
||||
}
|
||||
|
||||
const handleWaiting = () => {
|
||||
setIsBuffering(true)
|
||||
}
|
||||
|
||||
const handlePlaying = () => {
|
||||
setIsBuffering(false)
|
||||
setIsPlaying(true)
|
||||
}
|
||||
|
||||
const handlePause = () => {
|
||||
if (!isBuffering) {
|
||||
setIsPlaying(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
video.addEventListener("loadedmetadata", handleLoadedMetadata)
|
||||
video.addEventListener("timeupdate", handleTimeUpdate)
|
||||
video.addEventListener("waiting", handleWaiting)
|
||||
video.addEventListener("playing", handlePlaying)
|
||||
video.addEventListener("pause", handlePause)
|
||||
video.addEventListener("ended", handleEnded)
|
||||
|
||||
// 组件挂载时尝试播放视频
|
||||
const playVideo = async () => {
|
||||
try {
|
||||
await video.play()
|
||||
setIsPlaying(true)
|
||||
} catch (error) {
|
||||
console.error("自动播放失败:", error)
|
||||
}
|
||||
}
|
||||
|
||||
const timer = setTimeout(playVideo, 100)
|
||||
|
||||
return () => {
|
||||
video.removeEventListener("loadedmetadata", handleLoadedMetadata)
|
||||
video.removeEventListener("timeupdate", handleTimeUpdate)
|
||||
video.removeEventListener("waiting", handleWaiting)
|
||||
video.removeEventListener("playing", handlePlaying)
|
||||
video.removeEventListener("pause", handlePause)
|
||||
video.removeEventListener("ended", handleEnded)
|
||||
|
||||
// 清除所有定时器
|
||||
if (controlsTimerRef.current) {
|
||||
clearTimeout(controlsTimerRef.current)
|
||||
}
|
||||
if (mouseMoveTimerRef.current) {
|
||||
clearTimeout(mouseMoveTimerRef.current)
|
||||
}
|
||||
clearTimeout(timer)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 处理键盘事件
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (!videoRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
switch (e.code) {
|
||||
case "Space":
|
||||
e.preventDefault()
|
||||
const video = videoRef.current
|
||||
if (!video) return
|
||||
|
||||
if (isPlayingRef.current) {
|
||||
video.pause()
|
||||
setIsPlaying(false)
|
||||
} else {
|
||||
video.play().catch((error) => {
|
||||
console.error("播放失败:", error)
|
||||
})
|
||||
setIsPlaying(true)
|
||||
}
|
||||
break
|
||||
case "ArrowLeft":
|
||||
e.preventDefault()
|
||||
if (videoRef.current) {
|
||||
videoRef.current.currentTime = Math.max(
|
||||
0,
|
||||
videoRef.current.currentTime - 10
|
||||
)
|
||||
}
|
||||
break
|
||||
case "ArrowRight":
|
||||
e.preventDefault()
|
||||
if (videoRef.current && duration) {
|
||||
videoRef.current.currentTime = Math.min(
|
||||
duration,
|
||||
videoRef.current.currentTime + 10
|
||||
)
|
||||
}
|
||||
break
|
||||
case "ArrowUp":
|
||||
e.preventDefault()
|
||||
setVolume((prev) => {
|
||||
const newVolume = Math.min(1, prev + 0.1)
|
||||
if (videoRef.current) {
|
||||
videoRef.current.volume = newVolume
|
||||
setIsMuted(newVolume === 0)
|
||||
}
|
||||
return newVolume
|
||||
})
|
||||
break
|
||||
case "ArrowDown":
|
||||
e.preventDefault()
|
||||
setVolume((prev) => {
|
||||
const newVolume = Math.max(0, prev - 0.1)
|
||||
if (videoRef.current) {
|
||||
videoRef.current.volume = newVolume
|
||||
setIsMuted(newVolume === 0)
|
||||
}
|
||||
return newVolume
|
||||
})
|
||||
break
|
||||
case "KeyM":
|
||||
e.preventDefault()
|
||||
toggleMute()
|
||||
break
|
||||
case "KeyF":
|
||||
e.preventDefault()
|
||||
toggleFullscreen()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 键盘事件监听
|
||||
useEffect(() => {
|
||||
document.addEventListener("keydown", handleKeyDown)
|
||||
|
||||
if (containerRef.current) {
|
||||
containerRef.current.tabIndex = 0
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handleKeyDown)
|
||||
}
|
||||
}, [duration])
|
||||
|
||||
// 计算进度条百分比
|
||||
const progressPercent = duration ? (currentTime / duration) * 100 : 0
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="relative w-full h-screen bg-black flex justify-center items-center"
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseLeave={handleMouseLeave}>
|
||||
<video
|
||||
ref={videoRef}
|
||||
className="w-full h-full object-cover bg-black"
|
||||
onClick={togglePlayPause}
|
||||
playsInline
|
||||
preload="auto">
|
||||
<source src={iodVideo} type="video/mp4" />
|
||||
您的浏览器不支持HTML5视频播放
|
||||
</video>
|
||||
|
||||
{/* 暂停时的遮罩层 */}
|
||||
{!isPlaying && !isBuffering && (
|
||||
<div
|
||||
className="absolute inset-0 bg-black bg-opacity-30 flex items-center justify-center cursor-pointer"
|
||||
onClick={togglePlayPause}>
|
||||
<PlayCircleOutlined className="text-white text-6xl opacity-80" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 缓冲提示 */}
|
||||
{isBuffering && (
|
||||
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-white text-sm bg-black bg-opacity-50 px-4 py-2 rounded">
|
||||
缓冲中...
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{/* 控制栏 - 使用与原始HTML相同的类名和行为 */}
|
||||
<div
|
||||
className={`absolute bottom-0 left-0 w-full bg-gradient-to-t from-black to-transparent p-4 transition-all duration-300 ease-in-out flex flex-col gap-2.5 ${showControls ? "bottom-0" : "-bottom-32"}`}>
|
||||
<div className="flex items-center justify-end gap-2 cursor-pointer" onClick={handleEnded}>
|
||||
{<img src={logo} alt="logo" className="w-8" />}
|
||||
<h2 className="text-xl font-bold text-white dark:text-zinc-300 mr-3">
|
||||
<span className="text-[#d30100]">数联网</span>科创智能体
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="w-full h-1.5 bg-white bg-opacity-20 rounded cursor-pointer mb-2.5"
|
||||
onClick={handleProgressClick}>
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-orange-500 to-pink-600 rounded transition-all duration-100"
|
||||
style={{ width: `${progressPercent}%` }}></div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
className="bg-transparent border-none text-white text-lg cursor-pointer p-1 rounded-full w-12 h-12 flex items-center justify-center hover:bg-white hover:bg-opacity-20 transition-colors"
|
||||
onClick={togglePlayPause}>
|
||||
{isPlaying ? (
|
||||
<PauseCircleOutlined className="text-2xl" />
|
||||
) : (
|
||||
<PlayCircleOutlined className="text-2xl" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
<span className="text-white text-sm min-w-[100px] text-center">
|
||||
<span>{formatTime(currentTime)}</span> /
|
||||
<span>{formatTime(duration)}</span>
|
||||
</span>
|
||||
|
||||
<div className="flex items-center ml-auto">
|
||||
<button
|
||||
className="bg-transparent border-none text-white text-2xl cursor-pointer p-1 rounded-full w-12 h-12 flex items-center justify-center hover:bg-white hover:bg-opacity-20 transition-colors"
|
||||
onClick={toggleMute}>
|
||||
{isMuted ? "🔇" : "🔊"}
|
||||
</button>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.1"
|
||||
value={isMuted ? 0 : volume}
|
||||
onChange={handleVolumeChange}
|
||||
className="w-20 h-1.5 ml-2.5"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="bg-transparent border-none text-white text-lg cursor-pointer p-1 rounded-full w-12 h-12 flex items-center justify-center hover:bg-white hover:bg-opacity-20 transition-colors"
|
||||
onClick={toggleFullscreen}>
|
||||
<ExpandOutlined className="text-2xl" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default VideoPlayer
|
@ -60,7 +60,7 @@ export const importPageAssistData = async (file: File) => {
|
||||
}
|
||||
|
||||
if(data?.iod) {
|
||||
IodDb.getInstance().insertIodConnection(data.iod)
|
||||
IodDb.getInstance().insertIodConnection(data.iod.connection)
|
||||
}
|
||||
|
||||
message.success("Data imported successfully")
|
||||
|
@ -1,4 +1,5 @@
|
||||
import OptionLayout from "~/components/Layouts/Layout"
|
||||
import IodVideo from "@/components/Option/VideoPlayer/index.tsx"
|
||||
import { Playground } from "~/components/Option/Playground/Playground"
|
||||
|
||||
const OptionIndex = () => {
|
||||
|
Loading…
x
Reference in New Issue
Block a user