|
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) { |
|
|
|
const [cfg, setCfg] = useState<JobConfig>(() => { |
|
try { |
|
const draft = localStorage.getItem('cfgDraft'); |
|
if (draft) return JSON.parse(draft); |
|
} catch {} |
|
|
|
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 })); |
|
|
|
|
|
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 {} |
|
}, []); |
|
|
|
|
|
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]); |
|
|
|
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> |
|
); |
|
} |
|
|