import { useEffect, useCallback, useMemo } from 'react'; import { useInputBarStore } from '../store/inputBar'; import { useGenerationStore } from '../store/generation'; import { useAuthStore } from '../store/auth'; import { Dropdown } from './Dropdown'; import type { CreationMode, AspectRatio, Duration, GenerationType, ModelOption } from '../types'; import styles from './Toolbar.module.css'; const VideoIcon = () => ( ); const ImageIcon = () => ( ); const DiamondIcon = () => ( ); const LightningIcon = () => ( ); const StarIcon = () => ( ); const SwapIcon = () => ( ); const MonitorIcon = () => ( ); const ClockIcon = () => ( ); const ChevronDown = () => ( ); const generationTypeItems = [ { label: '视频生成', value: 'video' as GenerationType, icon: }, ]; const modelItems = [ { label: 'AirDrama', value: 'seedance_2.0' as ModelOption, icon: }, { label: 'AirDrama Fast', value: 'seedance_2.0_fast' as ModelOption, icon: }, ]; const modeItems = [ { label: '全能参考', value: 'universal' as CreationMode, icon: }, { label: '首尾帧', value: 'keyframe' as CreationMode, icon: }, ]; const ratioItems = [ { label: '21:9', value: '21:9' as AspectRatio }, { label: '16:9', value: '16:9' as AspectRatio }, { label: '4:3', value: '4:3' as AspectRatio }, { label: '1:1', value: '1:1' as AspectRatio }, { label: '3:4', value: '3:4' as AspectRatio }, { label: '9:16', value: '9:16' as AspectRatio }, ]; const keyframeRatioItems = [ ...ratioItems, ]; const durationItems = Array.from({ length: 12 }, (_, i) => { const v = i + 4; return { label: `${v}s`, value: String(v) }; }); const RESOLUTION_MAP: Record = { '16:9': [1280, 720], '9:16': [720, 1280], '4:3': [1112, 834], '1:1': [960, 960], '3:4': [834, 1112], '21:9': [1470, 630], }; const modeLabels: Record = { universal: '全能参考', keyframe: '首尾帧', }; export function Toolbar() { const generationType = useInputBarStore((s) => s.generationType); const setGenerationType = useInputBarStore((s) => s.setGenerationType); const model = useInputBarStore((s) => s.model); const setModel = useInputBarStore((s) => s.setModel); const mode = useInputBarStore((s) => s.mode); const switchMode = useInputBarStore((s) => s.switchMode); const aspectRatio = useInputBarStore((s) => s.aspectRatio); const setAspectRatio = useInputBarStore((s) => s.setAspectRatio); const duration = useInputBarStore((s) => s.duration); const setDuration = useInputBarStore((s) => s.setDuration); const isSubmittable = useInputBarStore((s) => s.canSubmit()); const triggerInsertAt = useInputBarStore((s) => s.triggerInsertAt); const isKeyframe = mode === 'keyframe'; const references = useInputBarStore((s) => s.references); const team = useAuthStore((s) => s.team); const addTask = useGenerationStore((s) => s.addTask); const estimatedTokens = useMemo(() => { const res = RESOLUTION_MAP[aspectRatio] || [1280, 720]; return Math.round((res[0] * res[1] * 24 * duration) / 1024); }, [aspectRatio, duration]); const estimatedCost = useMemo(() => { const hasVideoRef = references.some((r) => r.type === 'video'); let price = team?.token_price || 0; if (model === 'seedance_2.0_fast') { price = hasVideoRef ? (team?.token_price_fast_video || 0) : (team?.token_price_fast || 0); } else { price = hasVideoRef ? (team?.token_price_video || 0) : (team?.token_price || 0); } return (estimatedTokens * price / 1000000).toFixed(2); }, [estimatedTokens, model, references, team]); const handleSend = useCallback(() => { if (!isSubmittable) return; addTask(); }, [isSubmittable, addTask]); const handleInsertAt = useCallback(() => { triggerInsertAt(); }, [triggerInsertAt]); // Keyboard shortcut: Ctrl/Cmd + Enter useEffect(() => { const handler = (e: KeyboardEvent) => { if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { handleSend(); } }; document.addEventListener('keydown', handler); return () => document.removeEventListener('keydown', handler); }, [handleSend]); return (
{/* Generation type — fixed to video */} {/* Model selector */} setModel(v as ModelOption)} minWidth={160} trigger={ } /> {/* Mode selector */} switchMode(v as CreationMode)} minWidth={150} trigger={ } /> {/* Aspect ratio */} setAspectRatio(v as AspectRatio)} minWidth={100} trigger={ } /> {/* Duration */} setDuration(Number(v) as Duration)} minWidth={100} trigger={ } /> {/* @ button - universal mode only */} {!isKeyframe && ( )} {/* Spacer — push right group to the end */}
{/* 全部清空 + 预估消耗:仅有内容时显示 */} {isSubmittable && ( useInputBarStore.getState().reset()} style={{ fontSize: 12, color: '#8b8ea8', whiteSpace: 'nowrap', userSelect: 'none', cursor: 'pointer', transition: 'filter 0.15s', marginRight: 20, lineHeight: 1 }} onMouseEnter={(e) => { (e.currentTarget as HTMLElement).style.filter = 'brightness(1.4)'; }} onMouseLeave={(e) => { (e.currentTarget as HTMLElement).style.filter = ''; }} > ⟲ 全部清空 )} {/* Estimated cost */} {isSubmittable && (team?.token_price || 0) > 0 && ( 预估消耗:{estimatedTokens.toLocaleString()} tokens / ¥{estimatedCost} )} {/* Send button */}
); }