peihsin0715 commited on
Commit
444d92d
·
1 Parent(s): 4f1edc3
frontend/src/App.tsx CHANGED
@@ -3,13 +3,14 @@ import { Target } from 'lucide-react';
3
  import type { JobConfig, Extras } from './types';
4
  import { JobRunnerProvider, useJobRunner } from './hooks/JobRunnerProvider';
5
 
6
- const ConfigPage = lazy(() => import('./pages/ConfigPage'));
7
- const ResultsPage = lazy(() => import('./pages/ResultsPage'));
 
8
 
9
- type Tab = 'config'|'results'|'reports';
10
 
11
  function AppInner() {
12
- const [tab, setTab] = useState<Tab>('config');
13
  const { start } = useJobRunner();
14
 
15
  const run = (cfg: JobConfig, extras: Extras) => {
@@ -48,8 +49,15 @@ function AppInner() {
48
 
49
  <div className="border-b border-white/40 bg-white/30 backdrop-blur supports-[backdrop-filter]:bg-white/20">
50
  <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-3 flex gap-3">
51
- <button className={tabBtn(tab==='config')} onClick={() => setTab('config')}>Config Setting</button>
52
- <button className={tabBtn(tab==='results')} onClick={() => setTab('results')}>Results</button>
 
 
 
 
 
 
 
53
  </div>
54
  </div>
55
  </header>
@@ -60,8 +68,9 @@ function AppInner() {
60
  Loading
61
  </div>
62
  }>
63
- {/* 改成把 extras 一起傳 */}
64
- {tab === 'config' && <ConfigPage onRun={run} />}
 
65
  {tab === 'results' && <ResultsPage />}
66
  </Suspense>
67
  </main>
 
3
  import type { JobConfig, Extras } from './types';
4
  import { JobRunnerProvider, useJobRunner } from './hooks/JobRunnerProvider';
5
 
6
+ const DatasetConfigPage = lazy(() => import('./pages/DatasetConfigPage'));
7
+ const ModelConfigPage = lazy(() => import('./pages/ModelConfigPage'));
8
+ const ResultsPage = lazy(() => import('./pages/ResultsPage'));
9
 
10
+ type Tab = 'dataset' | 'model' | 'results';
11
 
12
  function AppInner() {
13
+ const [tab, setTab] = useState<Tab>('dataset');
14
  const { start } = useJobRunner();
15
 
16
  const run = (cfg: JobConfig, extras: Extras) => {
 
49
 
50
  <div className="border-b border-white/40 bg-white/30 backdrop-blur supports-[backdrop-filter]:bg-white/20">
51
  <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-3 flex gap-3">
52
+ <button className={tabBtn(tab==='dataset')} onClick={() => setTab('dataset')}>
53
+ Dataset Config
54
+ </button>
55
+ <button className={tabBtn(tab==='model')} onClick={() => setTab('model')}>
56
+ Model Config
57
+ </button>
58
+ <button className={tabBtn(tab==='results')} onClick={() => setTab('results')}>
59
+ Results
60
+ </button>
61
  </div>
62
  </div>
63
  </header>
 
68
  Loading
69
  </div>
70
  }>
71
+ {/* Dataset -> Model -> Results 流程:dataset 填完再進 model,model 送出後執行並進 results */}
72
+ {tab === 'dataset' && <DatasetConfigPage onNext={() => setTab('model')} />}
73
+ {tab === 'model' && <ModelConfigPage onRun={run} />}
74
  {tab === 'results' && <ResultsPage />}
75
  </Suspense>
76
  </main>
frontend/src/pages/{ConfigPage.tsx → DatasetConfigPage.tsx} RENAMED
@@ -1,16 +1,18 @@
1
  import { useEffect, useState } from 'react';
2
- import { Database, Bot, ExternalLink, Shuffle } from 'lucide-react';
3
  import DatasetValidator from '../components/validators/DatasetValidator';
4
- import ModelValidator from '../components/validators/ModelValidator';
5
  import { DATASETS } from '../constants/datasets';
6
- import { LM_MODELS} from '../constants/models';
7
  import type { JobConfig } from '../types';
8
 
9
- type Extras = {
10
- datasetLimit: number,
11
  };
12
 
13
- export default function ConfigPage({ onRun }: { onRun: (cfg: JobConfig, extras: Extras) => void }) {
 
 
 
 
14
  const [cfg, setCfg] = useState<JobConfig>({
15
  dataset: '',
16
  languageModel: '',
@@ -21,58 +23,52 @@ export default function ConfigPage({ onRun }: { onRun: (cfg: JobConfig, extras:
21
  tau: 0.1,
22
  iterations: 1000,
23
  seed: 42,
24
- enableFineTuning: false,
25
  counterfactual: false,
26
  });
27
 
28
- const [datasetLimit, setDatasetLimit] = useState<number>(10);
29
  const [customDataset, setCustomDataset] = useState('');
30
- const [customLM, setCustomLM] = useState('');
31
  const [showCustomDatasetInput, setShowCustomDatasetInput] = useState(false);
32
- const [showCustomLanguageInput, setShowCustomLanguageInput] = useState(false);
33
  const [fieldStats, setFieldStats] = useState<Record<string, Record<string, number>>>({});
34
  const [numCounterfactuals, setNumCounterfactuals] = useState<number>(3);
35
- const [classificationTask, setClassificationTask] = useState<'sentiment' | 'regard' | 'stereotype' | 'personality' | 'toxicity'>('sentiment');
36
- const [toxicityModelChoice, setToxicityModelChoice] = useState<'detoxify' | 'junglelee'>('detoxify');
37
  const [selectedCfFields, setSelectedCfFields] = useState<string[]>([]);
38
- const [availableFields, setAvailableFields] = useState<string[]>([]);
39
  const [isLoadingFields, setIsLoadingFields] = useState(false);
40
  const [fieldsError, setFieldsError] = useState<string | null>(null);
 
41
  const [metaConfigs, setMetaConfigs] = useState<string[]>([]);
42
  const [metaSplits, setMetaSplits] = useState<string[]>([]);
43
  const [selectedConfig, setSelectedConfig] = useState<string | null>(null);
44
  const [selectedSplit, setSelectedSplit] = useState<string>('train');
45
 
46
- const canStart = !!(cfg.dataset && cfg.languageModel);
47
- const [ftEpochs, setFtEpochs] = useState(3);
48
- const [ftBatchSize, setFtBatchSize] = useState(8);
49
- const [ftLR, setFtLR] = useState(5e-5);
50
  const setField = <K extends keyof JobConfig>(k: K, v: JobConfig[K]) =>
51
  setCfg((prev) => ({ ...prev, [k]: v }));
52
 
53
- const card = 'group relative rounded-2xl p-8 border border-white/30 bg-white/60 backdrop-blur-xl ' +
 
54
  'shadow-[0_15px_40px_-20px_rgba(30,41,59,0.35)] transition-all duration-300 ' +
55
  'hover:shadow-[0_20px_50px_-20px_rgba(79,70,229,0.45)] hover:-translate-y-0.5';
56
 
57
  const sectionTitle = 'text-xl font-bold tracking-tight text-slate-900';
58
  const subtext = 'text-sm text-slate-600';
59
- const fieldInput = 'w-full rounded-xl border-2 border-slate-200/70 bg-white/70 px-4 py-3 ' +
 
60
  'focus:outline-none focus:border-indigo-500 focus:ring-4 focus:ring-indigo-500/20 transition-all';
61
- const selectInput = 'w-full rounded-xl border-2 border-slate-200/70 bg-white/70 px-3 py-2.5 ' +
 
62
  'focus:outline-none focus:border-indigo-500 focus:ring-4 focus:ring-indigo-500/20 transition-all';
63
- const choiceRow = 'flex items-start gap-4 cursor-pointer p-4 rounded-xl border transition-colors ' +
 
64
  'bg-white/60 hover:bg-white/80 border-slate-200/60 hover:border-indigo-300';
65
 
66
- const currentDataset = DATASETS.find((d) => d.id === cfg.dataset);
67
- const fallbackFields: string[] = (currentDataset as any)?.fields || ['text', 'label', 'group'];
68
-
69
- const toggleCfField = (f: string) =>
70
- setSelectedCfFields((prev) =>
71
- (prev.includes(f) ? prev.filter((x) => x !== f) : [...prev, f])
72
- );
73
-
74
  const API_BASE = '/api';
75
-
 
 
 
 
 
 
76
  async function fetchJSON<T>(url: string, signal?: AbortSignal): Promise<T> {
77
  const fullURL = url.startsWith('http') ? url : `${API_BASE}${url}`;
78
  const res = await fetch(fullURL, { signal });
@@ -80,73 +76,59 @@ export default function ConfigPage({ onRun }: { onRun: (cfg: JobConfig, extras:
80
  return (await res.json()) as T;
81
  }
82
 
83
-
84
- function buildFieldsURL(datasetId: string, config: string | null, split: string): string {
85
- const params = new URLSearchParams();
86
- params.set('id', datasetId);
87
-
88
- if (config && config.trim() !== '') {
89
- params.set('config', config);
90
- }
91
-
92
- if (split && split.trim() !== '') {
93
- params.set('split', split);
94
- }
95
-
96
- return `/dataset/fields?${params.toString()}`;
97
- }
98
 
99
  useEffect(() => {
100
- console.log('📊 Dataset changed:', cfg.dataset);
101
  setSelectedCfFields([]);
102
  setFieldsError(null);
103
- setAvailableFields([]);
104
-
105
  if (!cfg.dataset || cfg.dataset === 'custom') return;
106
 
107
  const ac = new AbortController();
108
-
109
  const run = async () => {
110
  try {
111
- console.log('🔍 Fetching dataset meta...');
112
  const metaURL = `/dataset/meta?id=${encodeURIComponent(cfg.dataset)}`;
113
  const meta = await fetchJSON<{
114
  datasetId: string;
115
  configs: string[];
116
  splits: string[];
117
  }>(metaURL, ac.signal);
118
-
119
- console.log('📋 Meta data received:', meta);
120
-
121
  setMetaConfigs(meta.configs || []);
122
  setMetaSplits(meta.splits || []);
123
-
124
  const defaultConfig = meta.configs?.length ? meta.configs[0] : null;
125
- const defaultSplit = meta.splits?.length ?
126
- (meta.splits.includes('train') ? 'train' : meta.splits[0]) :
127
- 'train';
128
-
129
  setSelectedConfig(defaultConfig);
130
  setSelectedSplit(defaultSplit);
131
-
132
- console.log('🏷️ Fetching fields with config:', defaultConfig, 'split:', defaultSplit);
133
  setIsLoadingFields(true);
134
-
135
  const fieldsURL = buildFieldsURL(cfg.dataset, defaultConfig, defaultSplit);
136
  const fieldsData = await fetchJSON<{ fields: string[] }>(fieldsURL, ac.signal);
137
-
138
- setAvailableFields(fieldsData.fields || []);
139
  setFieldsError(null);
140
-
141
  } catch (err: any) {
142
- console.error('❌ Error in dataset effect:', err);
143
-
144
  setMetaConfigs([]);
145
  setMetaSplits([]);
146
  setSelectedConfig(null);
147
  setSelectedSplit('train');
148
-
149
- setAvailableFields([]);
150
  const fieldsURL = buildFieldsURL(cfg.dataset, null, 'train');
151
  setFieldsError(`(${fieldsURL}) → ${err?.message || '欄位讀取失敗'}`);
152
  } finally {
@@ -160,30 +142,25 @@ export default function ConfigPage({ onRun }: { onRun: (cfg: JobConfig, extras:
160
 
161
  useEffect(() => {
162
  if (!cfg.dataset || cfg.dataset === 'custom') return;
163
-
164
- console.log('🔄 Config/Split changed - config:', selectedConfig, 'split:', selectedSplit);
165
-
166
  const ac = new AbortController();
167
-
168
  const run = async () => {
169
  try {
170
  setIsLoadingFields(true);
171
-
 
172
  const fieldsURL = buildFieldsURL(cfg.dataset, selectedConfig, selectedSplit);
173
- const fieldsData = await fetchJSON<{ fields: string[] }>(fieldsURL, ac.signal);
174
-
175
- setAvailableFields(fieldsData.fields || []);
176
- setFieldsError(null);
177
- setSelectedCfFields([]);
178
 
 
179
  const statsURL = `/dataset/field-stats?id=${encodeURIComponent(cfg.dataset)}&field=domain&subfield=category`;
180
  const statsData = await fetchJSON<{ counts: Record<string, Record<string, number>> }>(statsURL, ac.signal);
181
- setFieldStats(statsData.counts || {});
182
-
 
183
  } catch (err: any) {
184
- console.error('❌ Error fetching fields after config/split change:', err);
185
  const fieldsURL = buildFieldsURL(cfg.dataset, selectedConfig, selectedSplit);
186
- setAvailableFields([]);
187
  setFieldsError(`(${fieldsURL}) → ${err?.message || 'Field Read Failed'}`);
188
  } finally {
189
  setIsLoadingFields(false);
@@ -194,10 +171,12 @@ export default function ConfigPage({ onRun }: { onRun: (cfg: JobConfig, extras:
194
  return () => ac.abort();
195
  }, [cfg.dataset, selectedConfig, selectedSplit]);
196
 
 
 
197
  return (
198
  <div className="space-y-10">
199
  <div className="grid grid-cols-1 lg:grid-cols-6 gap-8">
200
- {/* 數據集選擇 */}
201
  <div className={`${card} lg:col-span-3`}>
202
  <div className="flex items-center gap-3 mb-8">
203
  <div className="p-3 rounded-xl bg-gradient-to-br from-indigo-600 to-fuchsia-600 shadow-md shadow-indigo-600/30">
@@ -226,9 +205,9 @@ export default function ConfigPage({ onRun }: { onRun: (cfg: JobConfig, extras:
226
  <div className="font-semibold text-slate-900">{dataset.name}</div>
227
  <div className="flex items-center gap-4 text-xs text-slate-500 mt-2">
228
  {'entities' in dataset && (
229
- <span>📊 {(dataset as any).entities?.toLocaleString?.() || '-'} entities</span>
230
  )}
231
- {'groups' in dataset && <span>👥 {(dataset as any).groups || '-'} groups</span>}
232
  </div>
233
  <a
234
  href={`https://huggingface.co/datasets/${dataset.id}`}
@@ -244,7 +223,7 @@ export default function ConfigPage({ onRun }: { onRun: (cfg: JobConfig, extras:
244
  </label>
245
  ))}
246
 
247
- {/* 自訂數據集 */}
248
  <label className={choiceRow}>
249
  <input
250
  type="radio"
@@ -287,7 +266,7 @@ export default function ConfigPage({ onRun }: { onRun: (cfg: JobConfig, extras:
287
  </div>
288
  </div>
289
 
290
- {/* 反事實分析(置於中間欄) */}
291
  <div className={`${card} lg:col-span-3`}>
292
  <div className="flex items-center gap-3 mb-8">
293
  <div className="p-3 rounded-xl bg-gradient-to-br from-pink-600 to-rose-600 shadow-md shadow-pink-600/30">
@@ -297,27 +276,24 @@ export default function ConfigPage({ onRun }: { onRun: (cfg: JobConfig, extras:
297
  </div>
298
 
299
  <div className="space-y-6">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
300
 
301
- <div className="pt-2">
302
- <label className="block text-sm font-semibold text-slate-800 mb-1">
303
- Number of Counterfactual
304
- </label>
305
- <input
306
- type="number"
307
- min={1}
308
- max={20}
309
- step={1}
310
- value={numCounterfactuals}
311
- onChange={(e) => {
312
- const v = parseInt(e.target.value || '3', 10);
313
- setNumCounterfactuals(Number.isFinite(v) ? Math.max(1, Math.min(20, v)) : 3);
314
- }}
315
- className={fieldInput}
316
- />
317
- </div>
318
-
319
-
320
- {/* Dataset meta(若有 configs/splits 就顯示下拉) */}
321
  {(metaConfigs.length > 0 || metaSplits.length > 0) && (
322
  <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
323
  {metaConfigs.length > 0 && (
@@ -352,7 +328,6 @@ export default function ConfigPage({ onRun }: { onRun: (cfg: JobConfig, extras:
352
  </div>
353
  )}
354
 
355
- {/* 狀態列 */}
356
  <div className="text-xs text-slate-500 flex items-center gap-2">
357
  <span>Selected Dataset</span>
358
  <span className="inline-flex items-center rounded-full bg-slate-800/90 text-white px-2.5 py-1">
@@ -362,13 +337,17 @@ export default function ConfigPage({ onRun }: { onRun: (cfg: JobConfig, extras:
362
  {selectedSplit && <span className="ml-1">/ {selectedSplit}</span>}
363
  </div>
364
 
365
- {/* 欄位清單 */}
366
  <div>
367
  <div className="flex items-center justify-between mb-2">
368
  <div className="text-sm font-semibold text-slate-800">Optional fields</div>
369
  {isLoadingFields && <span className="text-xs text-slate-500">Loading</span>}
370
  </div>
371
 
 
 
 
 
372
  <div className="space-y-4 max-h-64 overflow-auto pr-1">
373
  {Object.entries(fieldStats).map(([domain, categories]) => (
374
  <div key={domain} className="bg-white/50 border border-slate-200 rounded-xl p-3 shadow-sm">
@@ -376,6 +355,7 @@ export default function ConfigPage({ onRun }: { onRun: (cfg: JobConfig, extras:
376
  <div className="grid grid-cols-1 sm:grid-cols-2 gap-x-4 gap-y-2 pl-1">
377
  {Object.entries(categories).map(([category, count]) => {
378
  const fieldKey = `${domain}/${category}`;
 
379
  return (
380
  <label
381
  key={fieldKey}
@@ -383,12 +363,10 @@ export default function ConfigPage({ onRun }: { onRun: (cfg: JobConfig, extras:
383
  >
384
  <input
385
  type="checkbox"
386
- checked={selectedCfFields.includes(fieldKey)}
387
  onChange={() =>
388
  setSelectedCfFields((prev) =>
389
- prev.includes(fieldKey)
390
- ? prev.filter((x) => x !== fieldKey)
391
- : [...prev, fieldKey]
392
  )
393
  }
394
  className="accent-fuchsia-600"
@@ -406,244 +384,36 @@ export default function ConfigPage({ onRun }: { onRun: (cfg: JobConfig, extras:
406
  </div>
407
  </div>
408
 
409
- {/* 模型選擇(包含 K / datasetLimit 與 metrictarget 的指定位置) */}
410
- <div className={`${card} lg:col-span-3`}>
411
- <div className="flex items-center gap-3 mb-8">
412
- <div className="p-3 rounded-xl bg-gradient-to-br from-emerald-600 to-teal-600 shadow-md shadow-emerald-600/30">
413
- <Bot className="w-6 h-6 text-white" />
414
- </div>
415
- <h3 className={sectionTitle}>Model Selection</h3>
416
- </div>
417
-
418
- <div className="space-y-8">
419
- {/* 語言模型 */}
420
- <div>
421
- <label className="block text-sm font-semibold text-slate-800 mb-2">🤖 Language Generation Model</label>
422
- <select
423
- value={cfg.languageModel}
424
- onChange={(e) => {
425
- setField('languageModel', e.target.value);
426
- setShowCustomLanguageInput(e.target.value === 'custom');
427
- }}
428
- className={selectInput}
429
- >
430
- <option value="">Select a Language Model</option>
431
- {LM_MODELS.map((m) => (
432
- <option key={m.id} value={m.id}>
433
- {m.name}({m.provider})
434
- </option>
435
- ))}
436
- <option value="custom">🔧 Custom Model Upload from Hugging Face</option>
437
- </select>
438
-
439
- {showCustomLanguageInput && (
440
- <input
441
- type="text"
442
- placeholder="Input Hugging Face Model ID (e.g.:microsoft/DialoGPT-medium)"
443
- value={customLM}
444
- onChange={(e) => {
445
- setCustomLM(e.target.value);
446
- setField('languageModel', e.target.value);
447
- }}
448
- className={`${fieldInput} mt-3`}
449
- />
450
- )}
451
-
452
- {(customLM || cfg.languageModel) && (
453
- <div className="mt-3">
454
- <ModelValidator modelId={customLM || cfg.languageModel} type="language" />
455
- </div>
456
- )}
457
-
458
- {/* 語言模型下方:K 與 datasetLimit */}
459
- <div className="mt-6 space-y-5">
460
- <div>
461
- <label className="block text-sm font-semibold text-slate-800 mb-1">
462
- Number of Candidates
463
- <span className="ml-2 text-xs font-normal text-slate-500">The number of candidates generated for each entity</span>
464
- </label>
465
- <input
466
- type="number"
467
- min={1}
468
- max={20}
469
- value={cfg.k}
470
- onChange={(e) => setField('k', parseInt(e.target.value || '0', 10))}
471
- className={fieldInput}
472
- />
473
- </div>
474
-
475
- <div>
476
- <label className="block text-sm font-semibold text-slate-800 mb-1">
477
- Testing Data Limit
478
- </label>
479
- <input
480
- type="number"
481
- min={1}
482
- max={10000}
483
- value={datasetLimit}
484
- onChange={(e) => setDatasetLimit(parseInt(e.target.value || '0', 10))}
485
- className={fieldInput}
486
- />
487
- </div>
488
- </div>
489
- </div>
490
-
491
- {/* 分類任務(固定選項) */}
492
- <div className="mt-6">
493
- <label className="block text-sm font-semibold text-slate-800 mb-1">
494
- 👻 Feature Extraction Model
495
- </label>
496
- <select
497
- value={classificationTask}
498
- onChange={(e) => setClassificationTask(e.target.value as any)}
499
- className={selectInput}
500
- >
501
- <option value="sentiment">Sentiment (0–1, Neutral ≈ 0.5)</option>
502
- <option value="regard">Regard (0–2, Neutral ≈ 1.0)</option>
503
- <option value="stereotype">Stereotype (0–1, Neutral ≈ 0.0)</option>
504
- <option value="personality">Personality (0–1, Neutral ≈ 0.2)</option>
505
- <option value="toxicity">Toxicity (0–1, Neutral ≈ 0.0)</option>
506
-
507
- </select>
508
- </div>
509
-
510
- {/* 毒性模型選擇(只有當任務為 toxicity 時顯示) */}
511
- {classificationTask === 'toxicity' && (
512
- <div className="mt-4">
513
- <label className="block text-sm font-semibold text-slate-800 mb-1">
514
- Toxicity Model Selection
515
- </label>
516
- <select
517
- value={toxicityModelChoice}
518
- onChange={(e) => setToxicityModelChoice(e.target.value as any)}
519
- className={selectInput}
520
- >
521
- <option value="detoxify">unitary/toxic-bert(detoxify)</option>
522
- <option value="junglelee">JungleLee/bert-toxic-comment-classification</option>
523
- </select>
524
- </div>
525
- )}
526
-
527
- {/* 評分模型下方:目標指標值 */}
528
- <div className="mt-6">
529
- <label className="block text-sm font-semibold text-slate-800 mb-1">
530
- Metric Target Value
531
- <span className="ml-2 text-xs font-normal text-slate-500">Indicator thresholds used to determine compliance</span>
532
- </label>
533
- <input
534
- type="number"
535
- min={0}
536
- max={2}
537
- step={0.01}
538
- value={cfg.metrictarget}
539
- onChange={(e) => setField('metrictarget', parseFloat(e.target.value || '0'))}
540
- className={fieldInput}
541
- />
542
-
543
-
544
- </div>
545
- </div>
546
- </div>
547
- {/* Fine-tuning 設定 */}
548
- <div className={`${card} lg:col-span-3`}>
549
- <div className="flex items-center gap-3 mb-8">
550
- <div className="p-3 rounded-xl bg-gradient-to-br from-orange-500 to-yellow-500 shadow-md shadow-orange-500/30">
551
- <Database className="w-6 h-6 text-white" />
552
- </div>
553
- <h3 className={sectionTitle}>Fine-tuning Setting</h3>
554
- </div>
555
-
556
- <div className="space-y-6">
557
- <label className="flex items-center gap-2">
558
- <input
559
- type="checkbox"
560
- checked={cfg.enableFineTuning}
561
- onChange={(e) => setField('enableFineTuning', e.target.checked)}
562
- className="accent-orange-500"
563
- />
564
- <span className="text-sm text-slate-800 font-semibold">Enable Fine-tuning</span>
565
- </label>
566
-
567
- {cfg.enableFineTuning && (
568
- <div className="space-y-4 pl-4 border-l-2 border-orange-200">
569
- {/* Epochs */}
570
- <div>
571
- <label className="block text-sm font-semibold text-slate-800 mb-1">
572
- Training Epochs
573
- </label>
574
- <input
575
- type="number"
576
- min={1}
577
- max={100}
578
- value={ftEpochs}
579
- onChange={(e) => setFtEpochs(parseInt(e.target.value || '3', 10))}
580
- className={fieldInput}
581
- />
582
- </div>
583
-
584
- {/* Batch Size */}
585
- <div>
586
- <label className="block text-sm font-semibold text-slate-800 mb-1">
587
- Batch Size
588
- </label>
589
- <input
590
- type="number"
591
- min={1}
592
- max={256}
593
- value={ftBatchSize}
594
- onChange={(e) => setFtBatchSize(parseInt(e.target.value || '8', 10))}
595
- className={fieldInput}
596
- />
597
- </div>
598
-
599
- {/* Learning Rate */}
600
- <div>
601
- <label className="block text-sm font-semibold text-slate-800 mb-1">
602
- Learning Rate
603
- </label>
604
- <input
605
- type="number"
606
- step={0.00001}
607
- value={ftLR}
608
- onChange={(e) => setFtLR(parseFloat(e.target.value || '0.00005'))}
609
- className={fieldInput}
610
- />
611
- </div>
612
- </div>
613
- )}
614
- </div>
615
- </div>
616
-
617
  </div>
618
 
619
- {/* 開始按鈕 */}
620
  <div className="flex">
621
  <button
622
  onClick={() => {
623
- const fullCfg = {
 
624
  ...cfg,
625
- selectedCfFields,
626
  numCounterfactuals,
627
- classificationTask,
628
- toxicityModelChoice,
629
- finetuneParams: {
630
- epochs: ftEpochs,
631
- batchSize: ftBatchSize,
632
- learningRate: ftLR,
633
- },
634
- };
635
- onRun(fullCfg, {
636
- datasetLimit
637
- });
638
  }}
639
- disabled={!canStart}
640
  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"
641
  >
642
- <span className="relative z-10">🚀 Start</span>
643
  <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%)]" />
644
  </button>
645
  </div>
646
-
647
  </div>
648
  );
649
- }
 
1
  import { useEffect, useState } from 'react';
2
+ import { Database, ExternalLink, Shuffle } from 'lucide-react';
3
  import DatasetValidator from '../components/validators/DatasetValidator';
 
4
  import { DATASETS } from '../constants/datasets';
 
5
  import type { JobConfig } from '../types';
6
 
7
+ type Props = {
8
+ onNext: () => void;
9
  };
10
 
11
+ type ExtrasDraft = {
12
+ datasetLimit?: number; // 由 Model 頁設定,這裡不動它
13
+ };
14
+
15
+ export default function DatasetConfigPage({ onNext }: Props) {
16
  const [cfg, setCfg] = useState<JobConfig>({
17
  dataset: '',
18
  languageModel: '',
 
23
  tau: 0.1,
24
  iterations: 1000,
25
  seed: 42,
26
+ enableFineTuning: false, // 需求指出 finetune 不要了,但保留欄位無害
27
  counterfactual: false,
28
  });
29
 
 
30
  const [customDataset, setCustomDataset] = useState('');
 
31
  const [showCustomDatasetInput, setShowCustomDatasetInput] = useState(false);
32
+
33
  const [fieldStats, setFieldStats] = useState<Record<string, Record<string, number>>>({});
34
  const [numCounterfactuals, setNumCounterfactuals] = useState<number>(3);
 
 
35
  const [selectedCfFields, setSelectedCfFields] = useState<string[]>([]);
 
36
  const [isLoadingFields, setIsLoadingFields] = useState(false);
37
  const [fieldsError, setFieldsError] = useState<string | null>(null);
38
+
39
  const [metaConfigs, setMetaConfigs] = useState<string[]>([]);
40
  const [metaSplits, setMetaSplits] = useState<string[]>([]);
41
  const [selectedConfig, setSelectedConfig] = useState<string | null>(null);
42
  const [selectedSplit, setSelectedSplit] = useState<string>('train');
43
 
 
 
 
 
44
  const setField = <K extends keyof JobConfig>(k: K, v: JobConfig[K]) =>
45
  setCfg((prev) => ({ ...prev, [k]: v }));
46
 
47
+ const card =
48
+ 'group relative rounded-2xl p-8 border border-white/30 bg-white/60 backdrop-blur-xl ' +
49
  'shadow-[0_15px_40px_-20px_rgba(30,41,59,0.35)] transition-all duration-300 ' +
50
  'hover:shadow-[0_20px_50px_-20px_rgba(79,70,229,0.45)] hover:-translate-y-0.5';
51
 
52
  const sectionTitle = 'text-xl font-bold tracking-tight text-slate-900';
53
  const subtext = 'text-sm text-slate-600';
54
+ const fieldInput =
55
+ 'w-full rounded-xl border-2 border-slate-200/70 bg-white/70 px-4 py-3 ' +
56
  'focus:outline-none focus:border-indigo-500 focus:ring-4 focus:ring-indigo-500/20 transition-all';
57
+ const selectInput =
58
+ 'w-full rounded-xl border-2 border-slate-200/70 bg-white/70 px-3 py-2.5 ' +
59
  'focus:outline-none focus:border-indigo-500 focus:ring-4 focus:ring-indigo-500/20 transition-all';
60
+ const choiceRow =
61
+ 'flex items-start gap-4 cursor-pointer p-4 rounded-xl border transition-colors ' +
62
  'bg-white/60 hover:bg-white/80 border-slate-200/60 hover:border-indigo-300';
63
 
 
 
 
 
 
 
 
 
64
  const API_BASE = '/api';
65
+ function buildFieldsURL(datasetId: string, config: string | null, split: string): string {
66
+ const params = new URLSearchParams();
67
+ params.set('id', datasetId);
68
+ if (config && config.trim() !== '') params.set('config', config);
69
+ if (split && split.trim() !== '') params.set('split', split);
70
+ return `/dataset/fields?${params.toString()}`;
71
+ }
72
  async function fetchJSON<T>(url: string, signal?: AbortSignal): Promise<T> {
73
  const fullURL = url.startsWith('http') ? url : `${API_BASE}${url}`;
74
  const res = await fetch(fullURL, { signal });
 
76
  return (await res.json()) as T;
77
  }
78
 
79
+ // 若 localStorage 之前有草稿,載入 (用於返回 Dataset 頁時保留狀態)
80
+ useEffect(() => {
81
+ try {
82
+ const draft = localStorage.getItem('cfgDraft');
83
+ if (draft) {
84
+ const parsed = JSON.parse(draft);
85
+ setCfg((prev) => ({ ...prev, ...parsed }));
86
+ if (parsed.numCounterfactuals) setNumCounterfactuals(parsed.numCounterfactuals);
87
+ if (parsed.selectedCfFields) setSelectedCfFields(parsed.selectedCfFields);
88
+ }
89
+ } catch {}
90
+ }, []);
 
 
 
91
 
92
  useEffect(() => {
 
93
  setSelectedCfFields([]);
94
  setFieldsError(null);
95
+
 
96
  if (!cfg.dataset || cfg.dataset === 'custom') return;
97
 
98
  const ac = new AbortController();
99
+
100
  const run = async () => {
101
  try {
 
102
  const metaURL = `/dataset/meta?id=${encodeURIComponent(cfg.dataset)}`;
103
  const meta = await fetchJSON<{
104
  datasetId: string;
105
  configs: string[];
106
  splits: string[];
107
  }>(metaURL, ac.signal);
108
+
 
 
109
  setMetaConfigs(meta.configs || []);
110
  setMetaSplits(meta.splits || []);
111
+
112
  const defaultConfig = meta.configs?.length ? meta.configs[0] : null;
113
+ const defaultSplit = meta.splits?.length
114
+ ? meta.splits.includes('train') ? 'train' : meta.splits[0]
115
+ : 'train';
116
+
117
  setSelectedConfig(defaultConfig);
118
  setSelectedSplit(defaultSplit);
119
+
 
120
  setIsLoadingFields(true);
 
121
  const fieldsURL = buildFieldsURL(cfg.dataset, defaultConfig, defaultSplit);
122
  const fieldsData = await fetchJSON<{ fields: string[] }>(fieldsURL, ac.signal);
123
+
124
+ // optional: 顯示 fields 清單 (目前以 fieldStats 呈現 domain/category)
125
  setFieldsError(null);
 
126
  } catch (err: any) {
 
 
127
  setMetaConfigs([]);
128
  setMetaSplits([]);
129
  setSelectedConfig(null);
130
  setSelectedSplit('train');
131
+
 
132
  const fieldsURL = buildFieldsURL(cfg.dataset, null, 'train');
133
  setFieldsError(`(${fieldsURL}) → ${err?.message || '欄位讀取失敗'}`);
134
  } finally {
 
142
 
143
  useEffect(() => {
144
  if (!cfg.dataset || cfg.dataset === 'custom') return;
145
+
 
 
146
  const ac = new AbortController();
 
147
  const run = async () => {
148
  try {
149
  setIsLoadingFields(true);
150
+
151
+ // 重新抓 fields
152
  const fieldsURL = buildFieldsURL(cfg.dataset, selectedConfig, selectedSplit);
153
+ await fetchJSON<{ fields: string[] }>(fieldsURL, ac.signal);
 
 
 
 
154
 
155
+ // 這裡使用你原本的統計 API 格式 (domain/category)
156
  const statsURL = `/dataset/field-stats?id=${encodeURIComponent(cfg.dataset)}&field=domain&subfield=category`;
157
  const statsData = await fetchJSON<{ counts: Record<string, Record<string, number>> }>(statsURL, ac.signal);
158
+ setFieldStats(statsData.counts || []);
159
+ setFieldsError(null);
160
+ setSelectedCfFields([]);
161
  } catch (err: any) {
 
162
  const fieldsURL = buildFieldsURL(cfg.dataset, selectedConfig, selectedSplit);
163
+ setFieldStats({});
164
  setFieldsError(`(${fieldsURL}) → ${err?.message || 'Field Read Failed'}`);
165
  } finally {
166
  setIsLoadingFields(false);
 
171
  return () => ac.abort();
172
  }, [cfg.dataset, selectedConfig, selectedSplit]);
173
 
174
+ const canNext = !!cfg.dataset;
175
+
176
  return (
177
  <div className="space-y-10">
178
  <div className="grid grid-cols-1 lg:grid-cols-6 gap-8">
179
+ {/* Dataset selection */}
180
  <div className={`${card} lg:col-span-3`}>
181
  <div className="flex items-center gap-3 mb-8">
182
  <div className="p-3 rounded-xl bg-gradient-to-br from-indigo-600 to-fuchsia-600 shadow-md shadow-indigo-600/30">
 
205
  <div className="font-semibold text-slate-900">{dataset.name}</div>
206
  <div className="flex items-center gap-4 text-xs text-slate-500 mt-2">
207
  {'entities' in dataset && (
208
+ <span>📊 {(dataset as any).entities?.toLocaleString?.() || '-' } entities</span>
209
  )}
210
+ {'groups' in dataset && <span>👥 {(dataset as any).groups || '-' } groups</span>}
211
  </div>
212
  <a
213
  href={`https://huggingface.co/datasets/${dataset.id}`}
 
223
  </label>
224
  ))}
225
 
226
+ {/* Custom dataset */}
227
  <label className={choiceRow}>
228
  <input
229
  type="radio"
 
266
  </div>
267
  </div>
268
 
269
+ {/* Counterfactual */}
270
  <div className={`${card} lg:col-span-3`}>
271
  <div className="flex items-center gap-3 mb-8">
272
  <div className="p-3 rounded-xl bg-gradient-to-br from-pink-600 to-rose-600 shadow-md shadow-pink-600/30">
 
276
  </div>
277
 
278
  <div className="space-y-6">
279
+ <div className="pt-2">
280
+ <label className="block text-sm font-semibold text-slate-800 mb-1">
281
+ Number of Counterfactual
282
+ </label>
283
+ <input
284
+ type="number"
285
+ min={1}
286
+ max={20}
287
+ step={1}
288
+ value={numCounterfactuals}
289
+ onChange={(e) => {
290
+ const v = parseInt(e.target.value || '3', 10);
291
+ setNumCounterfactuals(Number.isFinite(v) ? Math.max(1, Math.min(20, v)) : 3);
292
+ }}
293
+ className={fieldInput}
294
+ />
295
+ </div>
296
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
  {(metaConfigs.length > 0 || metaSplits.length > 0) && (
298
  <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
299
  {metaConfigs.length > 0 && (
 
328
  </div>
329
  )}
330
 
 
331
  <div className="text-xs text-slate-500 flex items-center gap-2">
332
  <span>Selected Dataset</span>
333
  <span className="inline-flex items-center rounded-full bg-slate-800/90 text-white px-2.5 py-1">
 
337
  {selectedSplit && <span className="ml-1">/ {selectedSplit}</span>}
338
  </div>
339
 
340
+ {/* Optional fields (domain/category) */}
341
  <div>
342
  <div className="flex items-center justify-between mb-2">
343
  <div className="text-sm font-semibold text-slate-800">Optional fields</div>
344
  {isLoadingFields && <span className="text-xs text-slate-500">Loading</span>}
345
  </div>
346
 
347
+ {!!fieldsError && (
348
+ <div className="text-xs text-rose-600 mb-2">{fieldsError}</div>
349
+ )}
350
+
351
  <div className="space-y-4 max-h-64 overflow-auto pr-1">
352
  {Object.entries(fieldStats).map(([domain, categories]) => (
353
  <div key={domain} className="bg-white/50 border border-slate-200 rounded-xl p-3 shadow-sm">
 
355
  <div className="grid grid-cols-1 sm:grid-cols-2 gap-x-4 gap-y-2 pl-1">
356
  {Object.entries(categories).map(([category, count]) => {
357
  const fieldKey = `${domain}/${category}`;
358
+ const checked = selectedCfFields.includes(fieldKey);
359
  return (
360
  <label
361
  key={fieldKey}
 
363
  >
364
  <input
365
  type="checkbox"
366
+ checked={checked}
367
  onChange={() =>
368
  setSelectedCfFields((prev) =>
369
+ checked ? prev.filter((x) => x !== fieldKey) : [...prev, fieldKey]
 
 
370
  )
371
  }
372
  className="accent-fuchsia-600"
 
384
  </div>
385
  </div>
386
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
387
  </div>
388
 
389
+ {/* Next */}
390
  <div className="flex">
391
  <button
392
  onClick={() => {
393
+ // Dataset/Counterfactual 的草稿寫入 localStorage,供 Model 頁讀取
394
+ const draft = {
395
  ...cfg,
396
+ selectedCfFields,
397
  numCounterfactuals,
398
+ // 保留使用者選的 meta config / split
399
+ datasetConfig: selectedConfig,
400
+ datasetSplit: selectedSplit,
401
+ } as any;
402
+ localStorage.setItem('cfgDraft', JSON.stringify(draft));
403
+
404
+ // 保留 extras(目前 Dataset 頁沒有調整 extras)
405
+ const extrasDraft: ExtrasDraft = JSON.parse(localStorage.getItem('extrasDraft') || '{}');
406
+ localStorage.setItem('extrasDraft', JSON.stringify(extrasDraft));
407
+
408
+ onNext();
409
  }}
410
+ disabled={!canNext}
411
  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"
412
  >
413
+ <span className="relative z-10">Next: Model Config</span>
414
  <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%)]" />
415
  </button>
416
  </div>
 
417
  </div>
418
  );
419
+ }
frontend/src/pages/ModelConfigPage.tsx ADDED
@@ -0,0 +1,281 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useState } from 'react';
2
+ import { Bot, Settings2 } from 'lucide-react';
3
+ import ModelValidator from '../components/validators/ModelValidator';
4
+ import { LM_MODELS } from '../constants/models';
5
+ import type { JobConfig } from '../types';
6
+
7
+ type Extras = {
8
+ datasetLimit: number;
9
+ };
10
+
11
+ type Props = {
12
+ onRun: (cfg: JobConfig, extras: Extras) => void;
13
+ };
14
+
15
+ export default function ModelConfigPage({ onRun }: Props) {
16
+ // 從 localStorage 載入 Dataset 頁的草稿
17
+ const [cfg, setCfg] = useState<JobConfig>(() => {
18
+ try {
19
+ const draft = localStorage.getItem('cfgDraft');
20
+ if (draft) return JSON.parse(draft);
21
+ } catch {}
22
+ // fallback 預設
23
+ return {
24
+ dataset: '',
25
+ languageModel: '',
26
+ scorerModel: '',
27
+ k: 5,
28
+ numCounterfactuals: 3,
29
+ metrictarget: 0.5,
30
+ tau: 0.1,
31
+ iterations: 1000,
32
+ seed: 42,
33
+ enableFineTuning: false,
34
+ counterfactual: false,
35
+ };
36
+ });
37
+
38
+ const [datasetLimit, setDatasetLimit] = useState<number>(() => {
39
+ try {
40
+ const extrasDraft = JSON.parse(localStorage.getItem('extrasDraft') || '{}');
41
+ return typeof extrasDraft.datasetLimit === 'number' ? extrasDraft.datasetLimit : 10;
42
+ } catch {
43
+ return 10;
44
+ }
45
+ });
46
+
47
+ const [customLM, setCustomLM] = useState('');
48
+ const [showCustomLanguageInput, setShowCustomLanguageInput] = useState(false);
49
+
50
+ const [classificationTask, setClassificationTask] = useState<
51
+ 'sentiment' | 'regard' | 'stereotype' | 'personality' | 'toxicity'
52
+ >('sentiment');
53
+ const [toxicityModelChoice, setToxicityModelChoice] = useState<'detoxify' | 'junglelee'>('detoxify');
54
+
55
+ const setField = <K extends keyof JobConfig>(k: K, v: JobConfig[K]) =>
56
+ setCfg((prev) => ({ ...prev, [k]: v }));
57
+
58
+ // 若有草稿中的 model 設定也載回
59
+ useEffect(() => {
60
+ try {
61
+ const draft = localStorage.getItem('cfgDraft');
62
+ if (!draft) return;
63
+ const parsed = JSON.parse(draft);
64
+ setClassificationTask(parsed.classificationTask ?? 'sentiment');
65
+ setToxicityModelChoice(parsed.toxicityModelChoice ?? 'detoxify');
66
+ if (parsed.languageModel) setField('languageModel', parsed.languageModel);
67
+ if (parsed.k) setField('k', parsed.k);
68
+ if (typeof parsed.metrictarget === 'number') setField('metrictarget', parsed.metrictarget);
69
+ } catch {}
70
+ }, []);
71
+
72
+ const card =
73
+ 'group relative rounded-2xl p-8 border border-white/30 bg-white/60 backdrop-blur-xl ' +
74
+ 'shadow-[0_15px_40px_-20px_rgba(30,41,59,0.35)] transition-all duration-300 ' +
75
+ 'hover:shadow-[0_20px_50px_-20px_rgba(79,70,229,0.45)] hover:-translate-y-0.5';
76
+ const sectionTitle = 'text-xl font-bold tracking-tight text-slate-900';
77
+ const fieldInput =
78
+ 'w-full rounded-xl border-2 border-slate-200/70 bg-white/70 px-4 py-3 ' +
79
+ 'focus:outline-none focus:border-indigo-500 focus:ring-4 focus:ring-indigo-500/20 transition-all';
80
+ const selectInput =
81
+ 'w-full rounded-xl border-2 border-slate-200/70 bg-white/70 px-3 py-2.5 ' +
82
+ 'focus:outline-none focus:border-indigo-500 focus:ring-4 focus:ring-indigo-500/20 transition-all';
83
+
84
+ const canRun = !!(cfg.dataset && (cfg.languageModel || customLM));
85
+
86
+ return (
87
+ <div className="space-y-10">
88
+ <div className="grid grid-cols-1 lg:grid-cols-6 gap-8">
89
+ {/* 卡片 1:Language Generation Model */}
90
+ <div className={`${card} lg:col-span-3`}>
91
+ <div className="flex items-center gap-3 mb-8">
92
+ <div className="p-3 rounded-xl bg-gradient-to-br from-emerald-600 to-teal-600 shadow-md shadow-emerald-600/30">
93
+ <Bot className="w-6 h-6 text-white" />
94
+ </div>
95
+ <h3 className={sectionTitle}>Language Generation Model</h3>
96
+ </div>
97
+
98
+ <div className="space-y-8">
99
+ <div>
100
+ <label className="block text-sm font-semibold text-slate-800 mb-2">Model</label>
101
+ <select
102
+ value={cfg.languageModel}
103
+ onChange={(e) => {
104
+ setField('languageModel', e.target.value);
105
+ setShowCustomLanguageInput(e.target.value === 'custom');
106
+ }}
107
+ className={selectInput}
108
+ >
109
+ <option value="">Select a Language Model</option>
110
+ {LM_MODELS.map((m) => (
111
+ <option key={m.id} value={m.id}>
112
+ {m.name}({m.provider})
113
+ </option>
114
+ ))}
115
+ <option value="custom">🔧 Custom Model Upload from Hugging Face</option>
116
+ </select>
117
+
118
+ {showCustomLanguageInput && (
119
+ <input
120
+ type="text"
121
+ placeholder="Input Hugging Face Model ID (e.g.: microsoft/DialoGPT-medium)"
122
+ value={customLM}
123
+ onChange={(e) => {
124
+ setCustomLM(e.target.value);
125
+ setField('languageModel', e.target.value);
126
+ }}
127
+ className={`${fieldInput} mt-3`}
128
+ />
129
+ )}
130
+
131
+ {(customLM || cfg.languageModel) && (
132
+ <div className="mt-3">
133
+ <ModelValidator modelId={customLM || cfg.languageModel} type="language" />
134
+ </div>
135
+ )}
136
+ </div>
137
+
138
+ {/* K 與 datasetLimit */}
139
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-5">
140
+ <div>
141
+ <label className="block text-sm font-semibold text-slate-800 mb-1">
142
+ Number of Candidates
143
+ <span className="ml-2 text-xs font-normal text-slate-500">
144
+ The number of candidates generated for each entity
145
+ </span>
146
+ </label>
147
+ <input
148
+ type="number"
149
+ min={1}
150
+ max={20}
151
+ value={cfg.k}
152
+ onChange={(e) => setField('k', parseInt(e.target.value || '0', 10))}
153
+ className={fieldInput}
154
+ />
155
+ </div>
156
+
157
+ <div>
158
+ <label className="block text-sm font-semibold text-slate-800 mb-1">
159
+ Testing Data Limit
160
+ </label>
161
+ <input
162
+ type="number"
163
+ min={1}
164
+ max={10000}
165
+ value={datasetLimit}
166
+ onChange={(e) => setDatasetLimit(parseInt(e.target.value || '0', 10))}
167
+ className={fieldInput}
168
+ />
169
+ </div>
170
+ </div>
171
+ </div>
172
+ </div>
173
+
174
+ {/* 卡片 2:Feature Extraction / Classification */}
175
+ <div className={`${card} lg:col-span-3`}>
176
+ <div className="flex items-center gap-3 mb-8">
177
+ <div className="p-3 rounded-xl bg-gradient-to-br from-indigo-600 to-fuchsia-600 shadow-md shadow-indigo-600/30">
178
+ <Settings2 className="w-6 h-6 text-white" />
179
+ </div>
180
+ <h3 className={sectionTitle}>Feature Extraction Model</h3>
181
+ </div>
182
+
183
+ <div className="space-y-8">
184
+ <div>
185
+ <label className="block text-sm font-semibold text-slate-800 mb-1">
186
+ Task
187
+ </label>
188
+ <select
189
+ value={classificationTask}
190
+ onChange={(e) => setClassificationTask(e.target.value as any)}
191
+ className={selectInput}
192
+ >
193
+ <option value="sentiment">Sentiment (0–1, Neutral ≈ 0.5)</option>
194
+ <option value="regard">Regard (0–2, Neutral ≈ 1.0)</option>
195
+ <option value="stereotype">Stereotype (0–1, Neutral ≈ 0.0)</option>
196
+ <option value="personality">Personality (0–1, Neutral ≈ 0.2)</option>
197
+ <option value="toxicity">Toxicity (0–1, Neutral ≈ 0.0)</option>
198
+ </select>
199
+ </div>
200
+
201
+ {classificationTask === 'toxicity' && (
202
+ <div>
203
+ <label className="block text-sm font-semibold text-slate-800 mb-1">
204
+ Toxicity Model
205
+ </label>
206
+ <select
207
+ value={toxicityModelChoice}
208
+ onChange={(e) => setToxicityModelChoice(e.target.value as any)}
209
+ className={selectInput}
210
+ >
211
+ <option value="detoxify">unitary/toxic-bert(detoxify)</option>
212
+ <option value="junglelee">JungleLee/bert-toxic-comment-classification</option>
213
+ </select>
214
+ </div>
215
+ )}
216
+
217
+ <div>
218
+ <label className="block text-sm font-semibold text-slate-800 mb-1">
219
+ Metric Target Value
220
+ <span className="ml-2 text-xs font-normal text-slate-500">
221
+ Indicator thresholds used to determine compliance
222
+ </span>
223
+ </label>
224
+ <input
225
+ type="number"
226
+ min={0}
227
+ max={2}
228
+ step={0.01}
229
+ value={cfg.metrictarget}
230
+ onChange={(e) => setField('metrictarget', parseFloat(e.target.value || '0'))}
231
+ className={fieldInput}
232
+ />
233
+ </div>
234
+ </div>
235
+ </div>
236
+ </div>
237
+
238
+ {/* Run */}
239
+ <div className="flex">
240
+ <button
241
+ onClick={() => {
242
+ // 讀回 Dataset 頁草稿並合併
243
+ let mergedCfg: JobConfig = { ...cfg };
244
+ try {
245
+ const draft = localStorage.getItem('cfgDraft');
246
+ if (draft) {
247
+ const parsed = JSON.parse(draft);
248
+ mergedCfg = { ...parsed, ...cfg, languageModel: cfg.languageModel || parsed.languageModel };
249
+ }
250
+ } catch {}
251
+
252
+ // 將目前 model 相關設定也寫回草稿,之後回到本頁仍可帶入
253
+ const persist = {
254
+ ...mergedCfg,
255
+ classificationTask,
256
+ toxicityModelChoice,
257
+ } as any;
258
+ localStorage.setItem('cfgDraft', JSON.stringify(persist));
259
+ localStorage.setItem('extrasDraft', JSON.stringify({ datasetLimit }));
260
+
261
+ onRun(
262
+ {
263
+ ...mergedCfg,
264
+ classificationTask,
265
+ toxicityModelChoice,
266
+ } as any,
267
+ {
268
+ datasetLimit,
269
+ }
270
+ );
271
+ }}
272
+ disabled={!canRun}
273
+ 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"
274
+ >
275
+ <span className="relative z-10">🚀 Start</span>
276
+ <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%)]" />
277
+ </button>
278
+ </div>
279
+ </div>
280
+ );
281
+ }