167 lines
5.6 KiB
TypeScript
167 lines
5.6 KiB
TypeScript
import { memo, useEffect, useRef, useState } from 'react'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Input } from '@/components/ui/input'
|
|
import { cn } from '@/utils/cn'
|
|
import type { SlideQuestion } from '../../../types'
|
|
|
|
interface SlideFormInteractionProps {
|
|
questions?: SlideQuestion[]
|
|
disabled?: boolean
|
|
messageTime?: string
|
|
onSendMessage?: (content: string) => void
|
|
}
|
|
|
|
const COUNTDOWN_SECONDS = 30
|
|
|
|
export const SlideFormInteraction = memo(
|
|
({
|
|
questions,
|
|
disabled,
|
|
messageTime,
|
|
onSendMessage,
|
|
}: SlideFormInteractionProps) => {
|
|
const [answers, setAnswers] = useState<Record<number, string>>({})
|
|
const [submitted, setSubmitted] = useState(false)
|
|
const [seconds, setSeconds] = useState(messageTime ? COUNTDOWN_SECONDS : 0)
|
|
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
|
|
|
// 初始化已有答案
|
|
useEffect(() => {
|
|
if (!questions) return
|
|
const initial: Record<number, string> = {}
|
|
questions.forEach((q, i) => {
|
|
if (q.answer != null) initial[i] = q.answer
|
|
})
|
|
setAnswers(initial)
|
|
}, [questions])
|
|
|
|
// 倒计时
|
|
useEffect(() => {
|
|
if (!messageTime || seconds <= 0) return
|
|
timerRef.current = setInterval(() => {
|
|
setSeconds(s => {
|
|
if (s <= 1) {
|
|
clearInterval(timerRef.current!)
|
|
handleSubmit()
|
|
return 0
|
|
}
|
|
return s - 1
|
|
})
|
|
}, 1000)
|
|
return () => { if (timerRef.current) clearInterval(timerRef.current) }
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [messageTime])
|
|
|
|
function handleSubmit() {
|
|
if (submitted) return
|
|
setSubmitted(true)
|
|
if (timerRef.current) clearInterval(timerRef.current)
|
|
|
|
const content = {
|
|
questions: questions?.map((q, idx) => ({
|
|
...q,
|
|
answer: answers[idx] ?? '',
|
|
})),
|
|
expected_user_action: 'slide_message_reply',
|
|
}
|
|
onSendMessage?.(JSON.stringify(content))
|
|
}
|
|
|
|
if (!questions?.length) return null
|
|
|
|
const isDisabled = disabled || submitted || seconds === 0
|
|
|
|
return (
|
|
<div
|
|
className={cn('mt-2 w-full overflow-hidden rounded-xl border border-border')}
|
|
style={{
|
|
background:
|
|
'radial-gradient(58% 12% at 97% 1%, var(--page-glow-2) 0%, rgba(0,0,0,0) 100%), radial-gradient(108% 26% at 10% -4%, var(--page-glow-1) 0%, rgba(0,0,0,0) 100%), var(--card)',
|
|
}}
|
|
>
|
|
{/* 标题 */}
|
|
<div className="flex items-center gap-1 px-3 py-2.5 font-medium text-primary">
|
|
<span className="text-sm">✦ 补充信息</span>
|
|
</div>
|
|
|
|
{/* 问题列表 */}
|
|
<div className="px-3 pb-3 flex flex-col gap-3">
|
|
{questions.map((q, idx) => (
|
|
<div key={idx} className="flex flex-col gap-1.5">
|
|
<label className="text-sm font-medium text-foreground">{q.question}</label>
|
|
|
|
{q.type === 'input' || !q.options?.length ? (
|
|
<Input
|
|
value={answers[idx] ?? ''}
|
|
onChange={e =>
|
|
setAnswers(prev => ({ ...prev, [idx]: e.target.value }))
|
|
}
|
|
disabled={isDisabled}
|
|
className="h-8 text-sm"
|
|
/>
|
|
) : (
|
|
<div className="flex flex-wrap gap-2">
|
|
{q.options.map(opt => {
|
|
const selected =
|
|
q.type === 'multiple'
|
|
? (answers[idx] ?? '').split(',').includes(opt)
|
|
: answers[idx] === opt
|
|
return (
|
|
<button
|
|
key={opt}
|
|
type="button"
|
|
disabled={isDisabled}
|
|
onClick={() => {
|
|
if (q.type === 'multiple') {
|
|
const current = (answers[idx] ?? '').split(',').filter(Boolean)
|
|
const next = selected
|
|
? current.filter(v => v !== opt)
|
|
: [...current, opt]
|
|
setAnswers(prev => ({ ...prev, [idx]: next.join(',') }))
|
|
} else {
|
|
setAnswers(prev => ({ ...prev, [idx]: opt }))
|
|
}
|
|
}}
|
|
className={cn(
|
|
'rounded-lg border px-3 py-1.5 text-sm transition-colors',
|
|
selected
|
|
? 'border-primary/40 bg-primary/10 text-primary'
|
|
: 'border-border/60 bg-secondary/60 text-foreground hover:bg-accent',
|
|
isDisabled && 'opacity-50 cursor-not-allowed',
|
|
)}
|
|
>
|
|
{opt}
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
|
|
{/* 底部:倒计时 + 提交 */}
|
|
<div className="flex items-center justify-between mt-1">
|
|
<span className="text-sm text-muted-foreground">
|
|
{submitted || seconds === 0 ? (
|
|
'已自动运行'
|
|
) : (
|
|
<>
|
|
<span className="text-primary">{seconds}s</span> 后自动运行
|
|
</>
|
|
)}
|
|
</span>
|
|
<Button
|
|
size="sm"
|
|
onClick={handleSubmit}
|
|
disabled={isDisabled}
|
|
className="text-sm h-7"
|
|
>
|
|
确认
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
},
|
|
)
|