All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m10s
①Seed 种子值全链路(后端传入/保存火山返回的seed/API返回,详情弹窗显示) ②前端种子值控件暂禁用(样式待调整) ③空页面文案改为品牌彩蛋 Every frame was once just air. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
163 lines
6.6 KiB
TypeScript
163 lines
6.6 KiB
TypeScript
import { useRef, useState, useCallback, type DragEvent } from 'react';
|
|
import { useInputBarStore } from '../store/inputBar';
|
|
import { UniversalUpload } from './UniversalUpload';
|
|
import { KeyframeUpload } from './KeyframeUpload';
|
|
import { PromptInput } from './PromptInput';
|
|
import { Toolbar } from './Toolbar';
|
|
import { AssetLibraryModal } from './AssetLibraryModal';
|
|
import { showToast } from './Toast';
|
|
import styles from './InputBar.module.css';
|
|
|
|
export function InputBar() {
|
|
const mode = useInputBarStore((s) => s.mode);
|
|
const addReferences = useInputBarStore((s) => s.addReferences);
|
|
const setFirstFrame = useInputBarStore((s) => s.setFirstFrame);
|
|
const barRef = useRef<HTMLDivElement>(null);
|
|
|
|
const handleDragOver = useCallback((e: DragEvent) => {
|
|
e.preventDefault();
|
|
// 只有外部文件拖入时才显示蓝色边框(内部 mention 标签拖拽不触发)
|
|
if (e.dataTransfer.types.includes('Files') && barRef.current) {
|
|
barRef.current.style.borderColor = '#00b8e6';
|
|
}
|
|
}, []);
|
|
|
|
const handleDragLeave = useCallback(() => {
|
|
if (barRef.current) {
|
|
barRef.current.style.borderColor = '#2a2a38';
|
|
}
|
|
}, []);
|
|
|
|
const handleDrop = useCallback((e: DragEvent) => {
|
|
e.preventDefault();
|
|
if (barRef.current) {
|
|
barRef.current.style.borderColor = '#2a2a38';
|
|
}
|
|
const IMAGE_MAX = 30 * 1024 * 1024;
|
|
const VIDEO_MAX = 50 * 1024 * 1024;
|
|
const AUDIO_MAX = 15 * 1024 * 1024;
|
|
const files = Array.from(e.dataTransfer.files).filter(
|
|
(f) => f.type.startsWith('image/') || f.type.startsWith('video/') || f.type.startsWith('audio/')
|
|
);
|
|
if (!files.length) return;
|
|
|
|
const valid: File[] = [];
|
|
for (const f of files) {
|
|
let limit: number;
|
|
let limitLabel: string;
|
|
if (f.type.startsWith('video/')) {
|
|
limit = VIDEO_MAX;
|
|
limitLabel = '视频文件不能超过50MB';
|
|
} else if (f.type.startsWith('audio/')) {
|
|
limit = AUDIO_MAX;
|
|
limitLabel = '音频文件不能超过15MB';
|
|
} else {
|
|
limit = IMAGE_MAX;
|
|
limitLabel = '图片文件不能超过30MB';
|
|
}
|
|
if (f.size > limit) {
|
|
showToast(limitLabel);
|
|
} else {
|
|
valid.push(f);
|
|
}
|
|
}
|
|
if (!valid.length) return;
|
|
|
|
if (mode === 'universal') {
|
|
addReferences(valid);
|
|
} else {
|
|
const imageFiles = valid.filter((f) => f.type.startsWith('image/'));
|
|
if (imageFiles.length > 0) {
|
|
setFirstFrame(imageFiles[0]);
|
|
}
|
|
}
|
|
}, [mode, addReferences, setFirstFrame]);
|
|
|
|
const [assetModalOpen, setAssetModalOpen] = useState(false);
|
|
const searchMode = useInputBarStore((s) => s.searchMode);
|
|
const setSearchMode = useInputBarStore((s) => s.setSearchMode);
|
|
const seed = useInputBarStore((s) => s.seed);
|
|
const seedEnabled = useInputBarStore((s) => s.seedEnabled);
|
|
const setSeed = useInputBarStore((s) => s.setSeed);
|
|
const setSeedEnabled = useInputBarStore((s) => s.setSeedEnabled);
|
|
const references = useInputBarStore((s) => s.references);
|
|
const editorHtml = useInputBarStore((s) => s.editorHtml);
|
|
const firstFrame = useInputBarStore((s) => s.firstFrame);
|
|
const lastFrame = useInputBarStore((s) => s.lastFrame);
|
|
|
|
// 联网搜索暂未开放
|
|
const searchDisabled = true;
|
|
|
|
return (
|
|
<div className={styles.wrapper}>
|
|
<div className={styles.container}>
|
|
{/* 素材库 + 联网搜索按钮 — 输入框上方 */}
|
|
<div style={{ display: 'flex', gap: 8, marginBottom: 6, paddingLeft: 4 }}>
|
|
<button
|
|
onClick={() => setAssetModalOpen(true)}
|
|
style={{
|
|
background: 'transparent', border: '1px solid var(--color-border-card)',
|
|
borderRadius: 6, padding: '4px 12px', fontSize: 12,
|
|
color: 'var(--color-text-secondary)', cursor: 'pointer',
|
|
transition: 'all 0.15s',
|
|
}}
|
|
onMouseEnter={(e) => { (e.currentTarget as HTMLElement).style.borderColor = 'var(--color-primary)'; (e.currentTarget as HTMLElement).style.color = 'var(--color-primary)'; }}
|
|
onMouseLeave={(e) => { (e.currentTarget as HTMLElement).style.borderColor = 'var(--color-border-card)'; (e.currentTarget as HTMLElement).style.color = 'var(--color-text-secondary)'; }}
|
|
>
|
|
素材库
|
|
</button>
|
|
<button
|
|
onClick={() => { if (!searchDisabled) setSearchMode(searchMode === 'smart' ? 'off' : 'smart'); }}
|
|
title={searchDisabled ? '联网搜索仅支持纯文生视频' : ''}
|
|
style={{
|
|
background: searchMode === 'smart' && !searchDisabled ? 'rgba(108, 99, 255, 0.12)' : 'transparent',
|
|
border: `1px solid ${searchMode === 'smart' && !searchDisabled ? 'var(--color-primary)' : 'var(--color-border-card)'}`,
|
|
borderRadius: 6, padding: '4px 12px', fontSize: 12,
|
|
color: searchDisabled ? '#3a3a4a' : searchMode === 'smart' ? 'var(--color-primary)' : 'var(--color-text-secondary)',
|
|
cursor: searchDisabled ? 'not-allowed' : 'pointer', transition: 'all 0.15s',
|
|
opacity: searchDisabled ? 0.5 : 1,
|
|
}}
|
|
onMouseEnter={(e) => { if (!searchDisabled && searchMode !== 'smart') { (e.currentTarget as HTMLElement).style.borderColor = 'var(--color-primary)'; (e.currentTarget as HTMLElement).style.color = 'var(--color-primary)'; } }}
|
|
onMouseLeave={(e) => { if (!searchDisabled && searchMode !== 'smart') { (e.currentTarget as HTMLElement).style.borderColor = 'var(--color-border-card)'; (e.currentTarget as HTMLElement).style.color = 'var(--color-text-secondary)'; } }}
|
|
>
|
|
联网搜索
|
|
</button>
|
|
<button
|
|
disabled
|
|
style={{
|
|
background: 'transparent',
|
|
border: '1px solid var(--color-border-card)',
|
|
borderRadius: 6, padding: '4px 12px', fontSize: 12,
|
|
color: '#3a3a4a', cursor: 'not-allowed', transition: 'all 0.15s',
|
|
opacity: 0.5,
|
|
}}
|
|
>
|
|
种子值
|
|
</button>
|
|
</div>
|
|
|
|
<div
|
|
ref={barRef}
|
|
className={styles.bar}
|
|
onDragOver={handleDragOver}
|
|
onDragLeave={handleDragLeave}
|
|
onDrop={handleDrop}
|
|
>
|
|
{/* Upper area: Upload + Prompt */}
|
|
<div className={styles.inputArea}>
|
|
{mode === 'universal' ? <UniversalUpload /> : <KeyframeUpload />}
|
|
<PromptInput />
|
|
</div>
|
|
|
|
{/* Divider */}
|
|
<div className={styles.divider} />
|
|
|
|
{/* Toolbar */}
|
|
<Toolbar />
|
|
</div>
|
|
</div>
|
|
<AssetLibraryModal open={assetModalOpen} onClose={() => setAssetModalOpen(false)} />
|
|
</div>
|
|
);
|
|
}
|