Files
Nexus_Mat/components/CreateModal.tsx

174 lines
8.3 KiB
TypeScript

import React, { useEffect, useState } from 'react';
import { DataService } from '../services/dataService';
import { GeminiService } from '../services/geminiService';
import { MaterialType, UserDTO } from '../types';
import { X, Wand2, UploadCloud } from 'lucide-react';
import { useToast } from './ToastProvider';
interface CreateModalProps {
onClose: () => void;
onSuccess: () => void;
}
export const CreateModal: React.FC<CreateModalProps> = ({ onClose, onSuccess }) => {
const toast = useToast();
const [formData, setFormData] = useState({
title: '',
description: '',
type: MaterialType.CODE,
codeSnippet: '',
tags: [] as string[]
});
const [file, setFile] = useState<File | null>(null);
const [currentUser, setCurrentUser] = useState<UserDTO | null>(null);
const [isProcessing, setIsProcessing] = useState(false);
useEffect(() => {
DataService.getCurrentUser().then(setCurrentUser).catch(() => setCurrentUser(null));
}, []);
const handleAutoGenerate = async () => {
if (!formData.codeSnippet) return;
setIsProcessing(true);
const { description, tags } = await GeminiService.analyzeCode(formData.codeSnippet);
setFormData(prev => ({
...prev,
description,
tags: [...prev.tags, ...tags]
}));
setIsProcessing(false);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsProcessing(true);
try {
if (formData.type === MaterialType.CODE) {
await DataService.createMaterial(formData);
} else if (formData.type === MaterialType.ASSET_ZIP) {
if (!file) throw new Error('Please select ZIP file');
if (file.size > 3 * 1024 * 1024) throw new Error('ZIP must be ≤ 3MB');
await DataService.uploadZip(file, { title: formData.title, description: formData.description, tags: formData.tags });
} else if (formData.type === MaterialType.VIDEO) {
if (!(currentUser?.role === 'MANAGER' || currentUser?.role === 'ADMIN')) throw new Error('Manager role required');
if (!file) throw new Error('Please select video file');
await DataService.uploadVideo(file, { title: formData.title, description: formData.description, tags: formData.tags });
}
onSuccess();
} catch (err: any) {
toast.error(err.message || 'Upload failed');
} finally {
setIsProcessing(false);
}
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm p-4">
<div className="bg-cyber-dark border border-cyber-neon/30 w-full max-w-2xl rounded-xl shadow-2xl relative overflow-hidden">
{/* Decorative Header */}
<div className="h-1 w-full bg-gradient-to-r from-cyber-neon via-cyber-blue to-cyber-pink"></div>
<div className="p-8">
<div className="flex justify-between items-center mb-8">
<h2 className="text-2xl font-mono font-bold text-white tracking-tight">UPLOAD_NEW_PROTOCOL</h2>
<button onClick={onClose} className="text-gray-500 hover:text-white"><X /></button>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
{/* Type Selection */}
<div className="flex gap-4">
{Object.values(MaterialType).map(t => {
const isVideo = t === MaterialType.VIDEO;
const allowed = !isVideo || (currentUser?.role === 'MANAGER' || currentUser?.role === 'ADMIN');
return (
<button
key={t}
type="button"
onClick={() => {
if (!allowed) { toast.error('Manager role required'); return; }
setFormData({ ...formData, type: t });
}}
className={`flex-1 py-3 text-xs font-mono border ${formData.type === t ? 'bg-cyber-neon/20 border-cyber-neon text-cyber-neon' : 'border-cyber-panel text-gray-500 hover:border-gray-500'} ${!allowed ? 'opacity-50 cursor-not-allowed' : ''}`}
>
{t}
</button>
);
})}
</div>
{/* Title */}
<div>
<label className="block text-xs font-mono text-gray-400 mb-1">TITLE_IDENTIFIER</label>
<input
required
className="w-full bg-black/50 border border-cyber-panel p-3 text-white focus:border-cyber-neon focus:outline-none"
value={formData.title}
onChange={e => setFormData({...formData, title: e.target.value})}
/>
</div>
{/* Content Input Based on Type */}
{formData.type === MaterialType.CODE ? (
<div className="relative">
<label className="block text-xs font-mono text-gray-400 mb-1">SOURCE_CODE</label>
<textarea
required
className="w-full h-40 bg-black/50 border border-cyber-panel p-3 text-xs font-mono text-cyber-blue focus:border-cyber-blue focus:outline-none"
value={formData.codeSnippet}
onChange={e => setFormData({...formData, codeSnippet: e.target.value})}
/>
{/* AI Magic Button */}
<button
type="button"
onClick={handleAutoGenerate}
disabled={!formData.codeSnippet || isProcessing}
className="absolute bottom-2 right-2 px-3 py-1 bg-cyber-blue/20 text-cyber-blue border border-cyber-blue/50 text-xs font-mono flex items-center gap-2 hover:bg-cyber-blue hover:text-black transition-colors"
>
<Wand2 size={12} /> {isProcessing ? 'SCANNING...' : 'AI_AUTO_FILL'}
</button>
</div>
) : (
<div>
<label className="block text-xs font-mono text-gray-400 mb-1">LOCAL_FILE {formData.type === MaterialType.ASSET_ZIP ? '(ZIP ≤ 3MB)' : '(Video ≤ 3MB)'}</label>
<label className="relative w-full block bg-black/50 border border-cyber-panel p-3 text-gray-400 hover:border-cyber-neon cursor-pointer transition-colors">
<input
required
type="file"
accept={formData.type === MaterialType.ASSET_ZIP ? '.zip,application/zip' : 'video/*,.mp4,.webm,.mov'}
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
onChange={e => setFile(e.target.files?.[0] || null)}
/>
<span className="flex items-center gap-2">
<UploadCloud size={16} />
{file ? file.name : '点击或拖拽文件到此处'}
</span>
</label>
</div>
)}
{/* Description */}
<div>
<label className="block text-xs font-mono text-gray-400 mb-1">DESCRIPTION</label>
<textarea
className="w-full h-24 bg-black/50 border border-cyber-panel p-3 text-sm text-gray-300 focus:border-cyber-neon focus:outline-none"
value={formData.description}
onChange={e => setFormData({...formData, description: e.target.value})}
/>
</div>
<button
type="submit"
disabled={isProcessing}
className="w-full py-4 bg-cyber-neon text-black font-bold font-mono tracking-widest hover:bg-white transition-colors flex items-center justify-center gap-2"
>
{isProcessing ? 'UPLOADING...' : <><UploadCloud size={20} /> INITIALIZE_UPLOAD</>}
</button>
</form>
</div>
</div>
</div>
);
};