Compare commits

..

No commits in common. "c937694d8b999c8f91d4de3c629bfc55f581d3fa" and "121dfabbd1f057637785495ce4ddf92dd114bcaf" have entirely different histories.

13 changed files with 128 additions and 580 deletions

Binary file not shown.

View File

@ -1,8 +1,7 @@
import { useForm } from "@mantine/form"
import React, { useEffect, useState } from "react"
import React from "react"
import { useTranslation } from "react-i18next"
import useDynamicTextareaSize from "~/hooks/useDynamicTextareaSize"
import TextArea from "antd/es/input/TextArea"
type Props = {
value: string
@ -15,14 +14,6 @@ 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: {
@ -38,27 +29,46 @@ export const EditMessageForm = (props: Props) => {
return (
<form
onSubmit={form.onSubmit((data) => {
if (isComposing) return
props.onClose()
props.onSumbit(value, true)
props.onSumbit(data.message, true)
})}
className="flex flex-col gap-2 w-96 ml-auto">
<TextArea
className="flex flex-col gap-2">
<textarea
{...form.getInputProps("message")}
onCompositionStart={() => setIsComposing(true)}
onCompositionEnd={() => setIsComposing(false)}
required
rows={2}
value={value}
rows={1}
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-end" : "justify-end"
!props.isBot ? "justify-between" : "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}
@ -66,12 +76,6 @@ 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>{" "}

View File

@ -1,5 +1,5 @@
import { Sidebar } from "@/components/Option/Sidebar.tsx"
import React, { useMemo } from "react"
import React, { useContext, 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 { useOptionLayoutContext } from "@/components/Layouts/Layout.tsx"
import { HistoryContext } 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 PlaygroundSidebar = () => {
export const PlaygroundHistory = () => {
const { setSystemPrompt } = useStoreChatModelSettings()
const { showOptionSidebar, setShowOptionSidebar, setShowVideo } = useOptionLayoutContext()
const { show, setShow } = useContext(HistoryContext)
const {
setMessages,
@ -55,8 +55,7 @@ export const PlaygroundSidebar = () => {
selectedModel,
setSelectedModel,
temporaryChat,
setSelectedSystemPrompt,
stopStreamingRequest
setSelectedSystemPrompt
} = useMessageOption()
const { t } = useTranslation(["option", "common", "settings"])
@ -78,9 +77,7 @@ export const PlaygroundSidebar = () => {
<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>
)
}
@ -121,21 +118,19 @@ export const PlaygroundSidebar = () => {
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" }}>
style={{ width: show ? "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>
{!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>
<button
className="text-gray-500 dark:text-gray-400"
onClick={() => {
setShowOptionSidebar(!showOptionSidebar)
setShow(!show)
}}>
<PanelLeftIcon className="w-6 h-6" />
</button>
@ -257,7 +252,7 @@ export const PlaygroundSidebar = () => {
</div>
<div className="overflow-y-auto flex-1 pl-7">
<Sidebar
onClose={() => setShowOptionSidebar(true)}
onClose={() => setShow(true)}
setMessages={setMessages}
setHistory={setHistory}
setHistoryId={setHistoryId}
@ -267,7 +262,6 @@ export const PlaygroundSidebar = () => {
historyId={historyId}
setSystemPrompt={setSystemPrompt}
temporaryChat={temporaryChat}
stopStreamingRequest={stopStreamingRequest}
history={history}
/>
</div>

View File

@ -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>
) : (
""

View File

@ -82,7 +82,7 @@ export const PlaygroundMessage: React.FC<Props> = (props) => {
</Tag>
)}
</div>
<div className={`flex flex-grow flex-col w-full`}>
<div className={`flex flex-grow flex-col`}>
{!editMode ? (
props.isBot ? (
<>
@ -239,7 +239,7 @@ export const PlaygroundMessage: React.FC<Props> = (props) => {
// : "flex"
}`}>
{props.isTTSEnabled && (
<Tooltip title={t("tts")} className="hidden">
<Tooltip title={t("tts")}>
<button
aria-label={t("tts")}
onClick={() => {
@ -297,7 +297,6 @@ export const PlaygroundMessage: React.FC<Props> = (props) => {
{props.generationInfo && (
<Popover
className="hidden"
content={
<GenerationInfo generationInfo={props.generationInfo} />
}
@ -323,8 +322,8 @@ export const PlaygroundMessage: React.FC<Props> = (props) => {
)}
</>
)}
{(!props.hideEditAndRegenerate && !props.isBot) && (
<Tooltip title={t("edit")} className="hidden">
{!props.hideEditAndRegenerate && (
<Tooltip title={t("edit")}>
<button
onClick={() => setEditMode(true)}
aria-label={t("edit")}
@ -334,7 +333,7 @@ export const PlaygroundMessage: React.FC<Props> = (props) => {
</Tooltip>
)}
{
<Tooltip title="收藏" className="hidden">
<Tooltip title="收藏">
<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">
@ -343,7 +342,7 @@ export const PlaygroundMessage: React.FC<Props> = (props) => {
</Tooltip>
}
{
<Tooltip title="发布语用" className="hidden">
<Tooltip title="发布语用">
<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">
@ -352,7 +351,7 @@ export const PlaygroundMessage: React.FC<Props> = (props) => {
</Tooltip>
}
{
<Tooltip title="发布对话" className="hidden">
<Tooltip title="发布对话">
<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">
@ -361,7 +360,7 @@ export const PlaygroundMessage: React.FC<Props> = (props) => {
</Tooltip>
}
{
<Tooltip title="点赞" className="hidden">
<Tooltip title="点赞">
<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">
@ -370,7 +369,7 @@ export const PlaygroundMessage: React.FC<Props> = (props) => {
</Tooltip>
}
{
<Tooltip title="点踩" className="hidden">
<Tooltip title="点踩">
<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">

View File

@ -1,5 +1,5 @@
import React, { useMemo, useState } from "react"
import { useOptionLayoutContext } from "@/components/Layouts/Layout.tsx"
import React, { useContext, useMemo, useState } from "react"
import { HistoryContext } from "@/components/Layouts/Layout.tsx"
import { PanelLeftIcon } from "lucide-react"
import { Button, Tooltip } from "antd"
import { PlusOutlined } from "@ant-design/icons"
@ -13,20 +13,22 @@ import { NotCollectIcon } from "@/components/Icons/NotCollect.tsx"
import { CollectIcon } from "@/components/Icons/Collect.tsx"
import { SettingIcon } from "@/components/Icons/Setting.tsx"
type Props = {}
type Props = {
setOpenModelSettings: (open: boolean) => void
}
export const Header: React.FC<Props> = ({}) => {
export const Header: React.FC<Props> = ({ setOpenModelSettings }) => {
const location = useLocation()
const { showOptionSidebar, setShowOptionSidebar } = useOptionLayoutContext()
const { show, setShow } = useContext(HistoryContext)
const showLeft = useMemo<boolean>(() => {
console.log(location.pathname)
if (location.pathname.includes("/settings")) {
return true
}
return showOptionSidebar
}, [location.pathname, showOptionSidebar])
return show
}, [location.pathname, show])
const { t } = useTranslation(["option", "common", "settings"])
@ -40,14 +42,14 @@ export const Header: React.FC<Props> = ({}) => {
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 ${showOptionSidebar && !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 ${show && !location.pathname.includes("/settings") ? "left-[300px]" : ""}`}>
{/*控制侧边栏显示隐藏与新建对话*/}
{!showLeft && (
<div className="flex items-center gap-3">
<button
className="text-gray-500 dark:text-gray-400"
onClick={() => {
setShowOptionSidebar(!showOptionSidebar)
setShow(!show)
}}>
<PanelLeftIcon className="w-6 h-6" />
</button>
@ -92,7 +94,7 @@ export const Header: React.FC<Props> = ({}) => {
w-[600px] h-[60px] dark:bg-black
flex items-center justify-center
transition-[top] drop-shadow
${showOptionSidebar ? "-top-[60px]" : "-top-[2px] delay-200"}
${show ? "-top-[60px]" : "-top-[2px] delay-200"}
`}>
<svg
className="icon"

View File

@ -1,86 +1,47 @@
import React, { useContext, useState } from "react"
import React, { useCallback, useEffect, useState } from "react"
import { CurrentChatModelSettings } from "../Common/Settings/CurrentChatModelSettings"
import { Header } from "./Header.tsx"
import IodVideo from "@/components/Option/VideoPlayer"
interface OptionLayoutContextType {
showOptionSidebar: boolean
setShowOptionSidebar: (show: boolean) => void
showVideo: boolean
setShowVideo: (show: boolean) => void
interface History {
show: boolean
setShow: (show: boolean) => void
}
const OptionLayoutContext = React.createContext<OptionLayoutContextType>({
showOptionSidebar: true,
setShowOptionSidebar: () => {},
showVideo: true,
setShowVideo: () => {}
export const HistoryContext = React.createContext<History>({
show: true,
setShow: () => {}
})
// 创建自定义 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"> */}
<OptionLayoutProvider>
<OptionLayoutMain>{children}</OptionLayoutMain>
</OptionLayoutProvider>
<HistoryContext.Provider value={historyContextValue}>
<Header setOpenModelSettings={setOpenModelSettings} />
{children}
</HistoryContext.Provider>
{/* </div> */}
<CurrentChatModelSettings
open={openModelSettings}
setOpen={setOpenModelSettings}

View File

@ -2,7 +2,6 @@ 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"
@ -16,6 +15,7 @@ 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]`}>
<PlaygroundSidebar />
<PlaygroundHistory />
<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,6 +48,9 @@ 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
@ -63,6 +66,7 @@ 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

@ -33,7 +33,6 @@ type Props = {
setSelectedModel: (model: string) => void
setSelectedSystemPrompt: (prompt: string) => void
setSystemPrompt: (prompt: string) => void
stopStreamingRequest: () => void
clearChat: () => void
temporaryChat: boolean
historyId: string
@ -47,7 +46,6 @@ export const Sidebar = ({
setHistoryId,
setSelectedModel,
setSelectedSystemPrompt,
stopStreamingRequest,
clearChat,
historyId,
setSystemPrompt,
@ -142,40 +140,6 @@ 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" : ""}`}>
@ -217,7 +181,38 @@ export const Sidebar = ({
)}
<button
className="flex-1 overflow-hidden break-all text-start truncate w-full"
onClick={() => handleHistoryClick(chat)}>
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">

View File

@ -1,410 +0,0 @@
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

View File

@ -60,7 +60,7 @@ export const importPageAssistData = async (file: File) => {
}
if(data?.iod) {
IodDb.getInstance().insertIodConnection(data.iod.connection)
IodDb.getInstance().insertIodConnection(data.iod)
}
message.success("Data imported successfully")

View File

@ -1,11 +1,10 @@
import OptionLayout from "~/components/Layouts/Layout"
import IodVideo from "@/components/Option/VideoPlayer/index.tsx"
import { Playground } from "~/components/Option/Playground/Playground"
const OptionIndex = () => {
return (
<OptionLayout>
<Playground />
<Playground />
</OptionLayout>
)
}