import { useState } from 'react'; import { useTranslation } from 'next-i18next'; import ApiKeyInput from './ApiKeyInput'; import EvolutionaryParams from './EvolutionaryParams'; export default function JobForm() { const { t } = useTranslation('common'); const [method, setMethod] = useState('linear'); const [mergeType, setMergeType] = useState('linear'); const [modelASource, setModelASource] = useState('hf'); const [modelBSource, setModelBSource] = useState('hf'); const [modelAId, setModelAId] = useState(''); const [modelBId, setModelBId] = useState(''); const [alpha, setAlpha] = useState(0.5); const [outputRepo, setOutputRepo] = useState(''); const [datasetFile, setDatasetFile] = useState(null); const [evoParams, setEvoParams] = useState({}); const [frankenLayers, setFrankenLayers] = useState(''); const [submitting, setSubmitting] = useState(false); const [errorMessage, setErrorMessage] = useState(null); // HuggingFace URL または Civitai URL から適切なIDを抽出する const normalizeRepoId = (input: string, source: string): string => { if (source === 'hf') { // "https://huggingface.co/namespace/repo" または "namespace/repo" のいずれか const match = input.match(/(?:huggingface\.co\/)?([^\/]+\/[^\/]+?)(?:\/resolve\/.*)?$/); if (match) return match[1]; } else if (source === 'civitai') { // Civitai モデルバージョンID(数字)を想定。URLなら抽出する const match = input.match(/models\/\d+\?modelVersionId=(\d+)/) || input.match(/model-versions\/(\d+)/); if (match) return match[1]; } // 何もマッチしなければそのまま返す(バックエンド側でエラーになるかもしれないが、元の動作を維持) return input; }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setSubmitting(true); setErrorMessage(null); const hfToken = sessionStorage.getItem('hf_token_manual') || ''; const civitaiKey = sessionStorage.getItem('civitai_key') || ''; if (!hfToken) { setErrorMessage(t('hf_token_required')); setSubmitting(false); return; } let datasetPath = ''; if (datasetFile) { const formData = new FormData(); formData.append('file', datasetFile); try { const res = await fetch('/api/backend/upload-dataset', { method: 'POST', body: formData }); if (!res.ok) { const err = await res.json(); throw new Error(err.detail || err.error || 'Dataset upload failed'); } const json = await res.json(); datasetPath = json.path; } catch (err: any) { setErrorMessage(err.message); setSubmitting(false); return; } } const payload = { model_a_source: modelASource, model_a_id: normalizeRepoId(modelAId, modelASource), model_b_source: modelBSource, model_b_id: normalizeRepoId(modelBId, modelBSource), method, merge_type: method === 'linear' ? mergeType : undefined, linear_alpha: alpha, output_repo_name: outputRepo, dataset: datasetPath, evo_params: method === 'evolutionary' ? evoParams : null, hf_token_manual: hfToken, civitai_key: civitaiKey, franken_layers: mergeType === 'franken' ? frankenLayers.split(',').map(s => s.trim()) : undefined, }; try { const res = await fetch('/api/backend/submit-job', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); if (!res.ok) { const data = await res.json(); throw new Error(data.error || data.detail || `Request failed with status ${res.status}`); } alert(t('job_submitted')); setModelAId(''); setModelBId(''); setOutputRepo(''); setDatasetFile(null); setFrankenLayers(''); } catch (err: any) { setErrorMessage(err.message); } finally { setSubmitting(false); } }; return (
{errorMessage && (
{errorMessage}
)}
setModelAId(e.target.value)} className="mt-1 block w-full border rounded p-2" required />
setModelBId(e.target.value)} className="mt-1 block w-full border rounded p-2" required />
{method === 'linear' && ( <>
{mergeType !== 'franken' && (
setAlpha(parseFloat(e.target.value))} className="w-full" />
)} {mergeType === 'franken' && (