RS-AAAI / frontend /src /pages /ModelConfigPage.tsx
peihsin0715
Add Title
adde4cd
import { useEffect, useState } from 'react';
import { Bot, Settings2, Lock } from 'lucide-react';
import ModelValidator from '../components/validators/ModelValidator';
import { LM_MODELS } from '../constants/models';
import type { JobConfig } from '../types';
type Extras = {
datasetLimit: number;
};
type Props = {
onRun: (cfg: JobConfig, extras: Extras) => void;
};
export default function ModelConfigPage({ onRun }: Props) {
// 從 localStorage 載入 Dataset 頁的草稿
const [cfg, setCfg] = useState<JobConfig>(() => {
try {
const draft = localStorage.getItem('cfgDraft');
if (draft) return JSON.parse(draft);
} catch {}
// fallback 預設
return {
dataset: '',
languageModel: '',
scorerModel: '',
k: 5,
numCounterfactuals: 3,
metrictarget: 0.5,
tau: 0.1,
iterations: 1000,
seed: 42,
enableFineTuning: false,
counterfactual: false,
};
});
const [datasetLimit, setDatasetLimit] = useState<number>(() => {
try {
const extrasDraft = JSON.parse(localStorage.getItem('extrasDraft') || '{}');
return typeof extrasDraft.datasetLimit === 'number' ? extrasDraft.datasetLimit : 10;
} catch {
return 10;
}
});
const [customLM, setCustomLM] = useState('');
const [showCustomLanguageInput, setShowCustomLanguageInput] = useState(false);
const [classificationTask, setClassificationTask] = useState<
'sentiment' | 'regard' | 'stereotype' | 'personality' | 'toxicity'
>('sentiment');
const [toxicityModelChoice, setToxicityModelChoice] = useState<'detoxify' | 'junglelee'>('detoxify');
const setField = <K extends keyof JobConfig>(k: K, v: JobConfig[K]) =>
setCfg((prev) => ({ ...prev, [k]: v }));
// 若有草稿中的 model 設定也載回
useEffect(() => {
try {
const draft = localStorage.getItem('cfgDraft');
if (!draft) return;
const parsed = JSON.parse(draft);
setClassificationTask(parsed.classificationTask ?? 'sentiment');
setToxicityModelChoice(parsed.toxicityModelChoice ?? 'detoxify');
if (parsed.languageModel) setField('languageModel', parsed.languageModel);
if (parsed.k) setField('k', parsed.k);
if (typeof parsed.metrictarget === 'number') setField('metrictarget', parsed.metrictarget);
} catch {}
}, []);
// 🔒 Example 鎖死規則:偵測到 example 就強制設定並關閉自定義輸入
const isExample = cfg.dataset === 'example';
useEffect(() => {
if (!isExample) return;
setField('languageModel', 'openai-community/gpt2'); // 後端預設也用這個
setShowCustomLanguageInput(false);
setCustomLM('');
setField('k', 10);
setClassificationTask('sentiment');
setField('metrictarget', 0.5);
}, [isExample]); // 當 dataset 改為/離開 example 時觸發
const card =
'group relative rounded-2xl p-8 border border-white/30 bg-white/60 backdrop-blur-xl ' +
'shadow-[0_15px_40px_-20px_rgba(30,41,59,0.35)] transition-all duration-300 ' +
'hover:shadow-[0_20px_50px_-20px_rgba(79,70,229,0.45)] hover:-translate-y-0.5';
const sectionTitle = 'text-xl font-bold tracking-tight text-slate-900';
const fieldInput =
'w-full rounded-xl border-2 border-slate-200/70 bg-white/70 px-4 py-3 ' +
'focus:outline-none focus:border-indigo-500 focus:ring-4 focus:ring-indigo-500/20 transition-all';
const selectInput =
'w-full rounded-xl border-2 border-slate-200/70 bg-white/70 px-3 py-2.5 ' +
'focus:outline-none focus:border-indigo-500 focus:ring-4 focus:ring-indigo-500/20 transition-all';
const canRun = !!(cfg.dataset && (cfg.languageModel || customLM));
return (
<div className="space-y-10">
<div className="grid grid-cols-1 lg:grid-cols-6 gap-8">
{/* 卡片 1:Language Generation Model */}
<div className={`${card} lg:col-span-3`}>
<div className="flex items-center gap-3 mb-8">
<div className="p-3 rounded-xl bg-gradient-to-br from-emerald-600 to-teal-600 shadow-md shadow-emerald-600/30">
<Bot className="w-6 h-6 text-white" />
</div>
<h3 className={sectionTitle}>Language Generation Model</h3>
{isExample && (
<span className="ml-2 inline-flex items-center gap-1 text-[11px] font-semibold px-2 py-0.5 rounded-full bg-slate-900 text-white">
<Lock className="w-3 h-3" /> Locked by Example
</span>
)}
</div>
<div className="space-y-8">
<div>
<label className="block text-sm font-semibold text-slate-800 mb-2">Model</label>
<select
value={isExample ? 'openai-community/gpt2' : cfg.languageModel}
onChange={(e) => {
if (isExample) return;
setField('languageModel', e.target.value);
setShowCustomLanguageInput(e.target.value === 'custom');
}}
disabled={isExample}
className={selectInput + (isExample ? ' cursor-not-allowed opacity-80' : '')}
>
<option value="">Select a Language Model</option>
{LM_MODELS.map((m) => (
<option key={m.id} value={m.id}>
{m.name}({m.provider})
</option>
))}
<option value="custom">🔧 Custom Model Upload from Hugging Face</option>
</select>
{!isExample && showCustomLanguageInput && (
<input
type="text"
placeholder="Input Hugging Face Model ID (e.g.: microsoft/DialoGPT-medium)"
value={customLM}
onChange={(e) => {
setCustomLM(e.target.value);
setField('languageModel', e.target.value);
}}
className={`${fieldInput} mt-3`}
/>
)}
{(isExample || customLM || cfg.languageModel) && (
<div className="mt-3">
<ModelValidator modelId={isExample ? 'openai-community/gpt2' : (customLM || cfg.languageModel)} type="language" />
</div>
)}
</div>
{/* K 與 datasetLimit */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
<div>
<label className="block text-sm font-semibold text-slate-800 mb-1">
Number of Candidates
<span className="ml-2 text-xs font-normal text-slate-500">
The number of candidates generated for each entity
</span>
</label>
<input
type="number"
min={1}
max={20}
value={isExample ? 10 : cfg.k}
onChange={(e) => {
if (isExample) return;
setField('k', parseInt(e.target.value || '0', 10));
}}
disabled={isExample}
className={fieldInput + (isExample ? ' cursor-not-allowed opacity-80' : '')}
/>
{isExample && (
<div className="text-[11px] mt-1 text-slate-500 flex items-center gap-1">
<Lock className="w-3 h-3" /> Locked to 10 for the Example preset.
</div>
)}
</div>
<div>
<label className="block text-sm font-semibold text-slate-800 mb-1">
Number of Data
</label>
<input
type="number"
min={1}
max={10000}
value={datasetLimit}
onChange={(e) => setDatasetLimit(parseInt(e.target.value || '0', 10))}
className={fieldInput}
/>
</div>
</div>
</div>
</div>
{/* 卡片 2:Feature Extraction / Classification */}
<div className={`${card} lg:col-span-3`}>
<div className="flex items-center gap-3 mb-8">
<div className="p-3 rounded-xl bg-gradient-to-br from-indigo-600 to-fuchsia-600 shadow-md shadow-indigo-600/30">
<Settings2 className="w-6 h-6 text-white" />
</div>
<h3 className={sectionTitle}>Feature Extraction Model</h3>
{isExample && (
<span className="ml-2 inline-flex items-center gap-1 text-[11px] font-semibold px-2 py-0.5 rounded-full bg-slate-900 text-white">
<Lock className="w-3 h-3" /> Locked by Example
</span>
)}
</div>
<div className="space-y-8">
<div>
<label className="block text-sm font-semibold text-slate-800 mb-1">
Task
</label>
<select
value={isExample ? 'sentiment' : classificationTask}
onChange={(e) => {
if (isExample) return;
setClassificationTask(e.target.value as any);
}}
disabled={isExample}
className={selectInput + (isExample ? ' cursor-not-allowed opacity-80' : '')}
>
<option value="sentiment">Sentiment (0–1, Neutral ≈ 0.5)</option>
<option value="regard">Regard (0–2, Neutral ≈ 1.0)</option>
<option value="stereotype">Stereotype (0–1, Neutral ≈ 0.0)</option>
<option value="personality">Personality (0–1, Neutral ≈ 0.2)</option>
<option value="toxicity">Toxicity (0–1, Neutral ≈ 0.0)</option>
</select>
</div>
{/* 只有 toxicity 才需要,example 強制 sentiment 就不顯示這塊 */}
{classificationTask === 'toxicity' && !isExample && (
<div>
<label className="block text-sm font-semibold text-slate-800 mb-1">
Toxicity Model
</label>
<select
value={toxicityModelChoice}
onChange={(e) => setToxicityModelChoice(e.target.value as any)}
className={selectInput}
>
<option value="detoxify">unitary/toxic-bert(detoxify)</option>
<option value="junglelee">JungleLee/bert-toxic-comment-classification</option>
</select>
</div>
)}
<div>
<label className="block text-sm font-semibold text-slate-800 mb-1">
Metric Target Value
<span className="ml-2 text-xs font-normal text-slate-500">
Indicator thresholds used to determine compliance
</span>
</label>
<input
type="number"
min={0}
max={2}
step={0.01}
value={isExample ? 0.5 : cfg.metrictarget}
onChange={(e) => {
if (isExample) return;
setField('metrictarget', parseFloat(e.target.value || '0'));
}}
disabled={isExample}
className={fieldInput + (isExample ? ' cursor-not-allowed opacity-80' : '')}
/>
{isExample && (
<div className="text-[11px] mt-1 text-slate-500 flex items-center gap-1">
<Lock className="w-3 h-3" /> Locked to 0.5 for the Example preset.
</div>
)}
</div>
</div>
</div>
</div>
{/* Run */}
<div className="flex">
<button
onClick={() => {
// 讀回 Dataset 頁草稿並合併
let mergedCfg: JobConfig = { ...cfg };
try {
const draft = localStorage.getItem('cfgDraft');
if (draft) {
const parsed = JSON.parse(draft);
mergedCfg = { ...parsed, ...cfg, languageModel: cfg.languageModel || parsed.languageModel };
}
} catch {}
// 🔒 若為 example,再次保險強制設定
if (isExample) {
mergedCfg = {
...mergedCfg,
languageModel: 'openai-community/gpt2',
k: 10,
metrictarget: 0.5,
} as JobConfig;
}
// 將目前 model 相關設定也寫回草稿,之後回到本頁仍可帶入
const persist = {
...mergedCfg,
classificationTask: isExample ? 'sentiment' : classificationTask,
toxicityModelChoice, // 不會用到,保留存檔
} as any;
localStorage.setItem('cfgDraft', JSON.stringify(persist));
localStorage.setItem('extrasDraft', JSON.stringify({ datasetLimit }));
onRun(
{
...mergedCfg,
classificationTask: isExample ? 'sentiment' : classificationTask,
toxicityModelChoice,
} as any,
{
datasetLimit,
}
);
}}
disabled={!canRun}
className="relative w-full group overflow-hidden rounded-2xl px-6 py-4 text-white font-semibold bg-gradient-to-r from-indigo-600 via-violet-600 to-fuchsia-600 shadow-lg shadow-indigo-600/20 enabled:hover:shadow-indigo-600/40 transition-all enabled:hover:translate-y-[-1px] enabled:active:translate-y-0 disabled:opacity-60 disabled:cursor-not-allowed"
>
<span className="relative z-10">🚀 Start</span>
<span className="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity bg-[radial-gradient(1200px_200px_at_50%_-40%,rgba(255,255,255,0.35),transparent_60%)]" />
</button>
</div>
</div>
);
}