Feat/web workflow improvements (#27981)
Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com> Co-authored-by: johnny0120 <johnny0120@users.noreply.github.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Wood <tuiskuwood@outlook.com>
This commit is contained in:
@@ -10,10 +10,11 @@ import { Theme } from '@/types/app'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type AudioPlayerProps = {
|
||||
src: string
|
||||
src?: string // Keep backward compatibility
|
||||
srcs?: string[] // Support multiple sources
|
||||
}
|
||||
|
||||
const AudioPlayer: React.FC<AudioPlayerProps> = ({ src }) => {
|
||||
const AudioPlayer: React.FC<AudioPlayerProps> = ({ src, srcs }) => {
|
||||
const [isPlaying, setIsPlaying] = useState(false)
|
||||
const [currentTime, setCurrentTime] = useState(0)
|
||||
const [duration, setDuration] = useState(0)
|
||||
@@ -61,19 +62,22 @@ const AudioPlayer: React.FC<AudioPlayerProps> = ({ src }) => {
|
||||
// Preload audio metadata
|
||||
audio.load()
|
||||
|
||||
// Delayed generation of waveform data
|
||||
// eslint-disable-next-line ts/no-use-before-define
|
||||
const timer = setTimeout(() => generateWaveformData(src), 1000)
|
||||
|
||||
return () => {
|
||||
audio.removeEventListener('loadedmetadata', setAudioData)
|
||||
audio.removeEventListener('timeupdate', setAudioTime)
|
||||
audio.removeEventListener('progress', handleProgress)
|
||||
audio.removeEventListener('ended', handleEnded)
|
||||
audio.removeEventListener('error', handleError)
|
||||
clearTimeout(timer)
|
||||
// Use the first source or src to generate waveform
|
||||
const primarySrc = srcs?.[0] || src
|
||||
if (primarySrc) {
|
||||
// Delayed generation of waveform data
|
||||
// eslint-disable-next-line ts/no-use-before-define
|
||||
const timer = setTimeout(() => generateWaveformData(primarySrc), 1000)
|
||||
return () => {
|
||||
audio.removeEventListener('loadedmetadata', setAudioData)
|
||||
audio.removeEventListener('timeupdate', setAudioTime)
|
||||
audio.removeEventListener('progress', handleProgress)
|
||||
audio.removeEventListener('ended', handleEnded)
|
||||
audio.removeEventListener('error', handleError)
|
||||
clearTimeout(timer)
|
||||
}
|
||||
}
|
||||
}, [src])
|
||||
}, [src, srcs])
|
||||
|
||||
const generateWaveformData = async (audioSrc: string) => {
|
||||
if (!window.AudioContext && !(window as any).webkitAudioContext) {
|
||||
@@ -85,8 +89,9 @@ const AudioPlayer: React.FC<AudioPlayerProps> = ({ src }) => {
|
||||
return null
|
||||
}
|
||||
|
||||
const url = new URL(src)
|
||||
const isHttp = url.protocol === 'http:' || url.protocol === 'https:'
|
||||
const primarySrc = srcs?.[0] || src
|
||||
const url = primarySrc ? new URL(primarySrc) : null
|
||||
const isHttp = url ? (url.protocol === 'http:' || url.protocol === 'https:') : false
|
||||
if (!isHttp) {
|
||||
setIsAudioAvailable(false)
|
||||
return null
|
||||
@@ -286,8 +291,13 @@ const AudioPlayer: React.FC<AudioPlayerProps> = ({ src }) => {
|
||||
}, [duration])
|
||||
|
||||
return (
|
||||
<div className='flex h-9 min-w-[240px] max-w-[420px] items-end gap-2 rounded-[10px] border border-components-panel-border-subtle bg-components-chat-input-audio-bg-alt p-2 shadow-xs backdrop-blur-sm'>
|
||||
<audio ref={audioRef} src={src} preload="auto"/>
|
||||
<div className='flex h-9 min-w-[240px] max-w-[420px] items-center gap-2 rounded-[10px] border border-components-panel-border-subtle bg-components-chat-input-audio-bg-alt p-2 shadow-xs backdrop-blur-sm'>
|
||||
<audio ref={audioRef} src={src} preload="auto">
|
||||
{/* If srcs array is provided, render multiple source elements */}
|
||||
{srcs && srcs.map((srcUrl, index) => (
|
||||
<source key={index} src={srcUrl} />
|
||||
))}
|
||||
</audio>
|
||||
<button type="button" className='inline-flex shrink-0 cursor-pointer items-center justify-center border-none text-text-accent transition-all hover:text-text-accent-secondary disabled:text-components-button-primary-bg-disabled' onClick={togglePlay} disabled={!isAudioAvailable}>
|
||||
{isPlaying
|
||||
? (
|
||||
|
||||
@@ -6,7 +6,14 @@ type Props = {
|
||||
}
|
||||
|
||||
const AudioGallery: React.FC<Props> = ({ srcs }) => {
|
||||
return (<><br/>{srcs.map((src, index) => (<AudioPlayer key={`audio_${index}`} src={src}/>))}</>)
|
||||
const validSrcs = srcs.filter(src => src)
|
||||
if (validSrcs.length === 0) return null
|
||||
|
||||
return (
|
||||
<div className="my-3">
|
||||
<AudioPlayer srcs={validSrcs} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(AudioGallery)
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
import Textarea from 'react-textarea-autosize'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Recorder from 'js-audio-recorder'
|
||||
import { decode } from 'html-entities'
|
||||
import type {
|
||||
EnableType,
|
||||
OnSend,
|
||||
@@ -203,7 +204,7 @@ const ChatInputArea = ({
|
||||
className={cn(
|
||||
'body-lg-regular w-full resize-none bg-transparent p-1 leading-6 text-text-primary outline-none',
|
||||
)}
|
||||
placeholder={t('common.chat.inputPlaceholder', { botName }) || ''}
|
||||
placeholder={decode(t('common.chat.inputPlaceholder', { botName }) || '')}
|
||||
autoFocus
|
||||
minRows={1}
|
||||
value={query}
|
||||
|
||||
@@ -16,6 +16,7 @@ import type { InputVar } from '@/app/components/workflow/types'
|
||||
import { getNewVar } from '@/utils/var'
|
||||
import cn from '@/utils/classnames'
|
||||
import { noop } from 'lodash-es'
|
||||
import { checkKeys } from '@/utils/var'
|
||||
|
||||
type OpeningSettingModalProps = {
|
||||
data: OpeningStatement
|
||||
@@ -53,7 +54,10 @@ const OpeningSettingModal = ({
|
||||
return
|
||||
|
||||
if (!ignoreVariablesCheck) {
|
||||
const keys = getInputKeys(tempValue)
|
||||
const keys = getInputKeys(tempValue)?.filter((key) => {
|
||||
const { isValid } = checkKeys([key], true)
|
||||
return isValid
|
||||
})
|
||||
const promptKeys = promptVariables.map(item => item.key)
|
||||
const workflowVariableKeys = workflowVariables.map(item => item.variable)
|
||||
let notIncludeKeys: string[] = []
|
||||
|
||||
@@ -2,7 +2,8 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import styles from './VideoPlayer.module.css'
|
||||
|
||||
type VideoPlayerProps = {
|
||||
src: string
|
||||
src?: string // Keep backward compatibility
|
||||
srcs?: string[] // Support multiple sources
|
||||
}
|
||||
|
||||
const PlayIcon = () => (
|
||||
@@ -35,7 +36,7 @@ const FullscreenIcon = () => (
|
||||
</svg>
|
||||
)
|
||||
|
||||
const VideoPlayer: React.FC<VideoPlayerProps> = ({ src }) => {
|
||||
const VideoPlayer: React.FC<VideoPlayerProps> = ({ src, srcs }) => {
|
||||
const [isPlaying, setIsPlaying] = useState(false)
|
||||
const [currentTime, setCurrentTime] = useState(0)
|
||||
const [duration, setDuration] = useState(0)
|
||||
@@ -78,7 +79,7 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({ src }) => {
|
||||
video.removeEventListener('timeupdate', setVideoTime)
|
||||
video.removeEventListener('ended', handleEnded)
|
||||
}
|
||||
}, [src])
|
||||
}, [src, srcs])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
@@ -131,7 +132,7 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({ src }) => {
|
||||
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
const updateVideoProgress = useCallback((clientX: number) => {
|
||||
const updateVideoProgress = useCallback((clientX: number, updateTime = false) => {
|
||||
const progressBar = progressRef.current
|
||||
const video = videoRef.current
|
||||
if (progressBar && video) {
|
||||
@@ -140,7 +141,7 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({ src }) => {
|
||||
const newTime = pos * video.duration
|
||||
if (newTime >= 0 && newTime <= video.duration) {
|
||||
setHoverTime(newTime)
|
||||
if (isDragging)
|
||||
if (isDragging || updateTime)
|
||||
video.currentTime = newTime
|
||||
}
|
||||
}
|
||||
@@ -155,10 +156,15 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({ src }) => {
|
||||
setHoverTime(null)
|
||||
}, [isDragging])
|
||||
|
||||
const handleProgressClick = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.preventDefault()
|
||||
updateVideoProgress(e.clientX, true)
|
||||
}, [updateVideoProgress])
|
||||
|
||||
const handleMouseDown = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.preventDefault()
|
||||
setIsDragging(true)
|
||||
updateVideoProgress(e.clientX)
|
||||
updateVideoProgress(e.clientX, true)
|
||||
}, [updateVideoProgress])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -209,14 +215,19 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({ src }) => {
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className={styles.videoPlayer} onMouseMove={showControls} onMouseEnter={showControls}>
|
||||
<video ref={videoRef} src={src} className={styles.video} />
|
||||
<video ref={videoRef} src={src} className={styles.video}>
|
||||
{/* If srcs array is provided, render multiple source elements */}
|
||||
{srcs && srcs.map((srcUrl, index) => (
|
||||
<source key={index} src={srcUrl} />
|
||||
))}
|
||||
</video>
|
||||
<div className={`${styles.controls} ${isControlsVisible ? styles.visible : styles.hidden} ${isSmallSize ? styles.smallSize : ''}`}>
|
||||
<div className={styles.overlay}>
|
||||
<div className={styles.progressBarContainer}>
|
||||
<div
|
||||
ref={progressRef}
|
||||
className={styles.progressBar}
|
||||
onClick={handleMouseDown}
|
||||
onClick={handleProgressClick}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onMouseDown={handleMouseDown}
|
||||
|
||||
@@ -6,7 +6,14 @@ type Props = {
|
||||
}
|
||||
|
||||
const VideoGallery: React.FC<Props> = ({ srcs }) => {
|
||||
return (<><br/>{srcs.map((src, index) => (<React.Fragment key={`video_${index}`}><br/><VideoPlayer src={src}/></React.Fragment>))}</>)
|
||||
const validSrcs = srcs.filter(src => src)
|
||||
if (validSrcs.length === 0) return null
|
||||
|
||||
return (
|
||||
<div className="my-3">
|
||||
<VideoPlayer srcs={validSrcs} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(VideoGallery)
|
||||
|
||||
Reference in New Issue
Block a user