peihsin0715 commited on
Commit
ad9612f
·
1 Parent(s): 444d92d

Add example

Browse files
backend/requirements.txt CHANGED
@@ -161,4 +161,5 @@ wsproto==1.2.0
161
  xgboost==3.0.0
162
  xxhash==3.5.0
163
  yarl==1.9.4
164
- seaborn
 
 
161
  xgboost==3.0.0
162
  xxhash==3.5.0
163
  yarl==1.9.4
164
+ seaborn
165
+ plotly
backend/server.py CHANGED
@@ -80,6 +80,8 @@ def serve_data(filename):
80
  mimetype = 'image/jpeg'
81
  elif filename.endswith('.csv'):
82
  mimetype = 'text/csv'
 
 
83
  else:
84
  mimetype = 'application/octet-stream'
85
 
@@ -519,10 +521,9 @@ def run_pipeline():
519
  print("[Plot check exists]", cf_path, os.path.exists(cf_path))
520
 
521
  results['plots'] = {
522
- 'original_sentiment': f"/tmp/{orig_sent_title}.png",
523
- 'counterfactual_sentiment': f"/tmp/{cf_sent_title}.png",
524
  }
525
-
526
  print("[Plot urls]", results['plots'])
527
 
528
  if config.get("enableFineTuning"):
 
80
  mimetype = 'image/jpeg'
81
  elif filename.endswith('.csv'):
82
  mimetype = 'text/csv'
83
+ elif filename.endswith('.html'): # 👈 新增這行
84
+ mimetype = 'text/html; charset=utf-8'
85
  else:
86
  mimetype = 'application/octet-stream'
87
 
 
521
  print("[Plot check exists]", cf_path, os.path.exists(cf_path))
522
 
523
  results['plots'] = {
524
+ 'original_sentiment': f"/tmp/{os.path.basename(orig_path)}",
525
+ 'counterfactual_sentiment': f"/tmp/{os.path.basename(cf_path)}",
526
  }
 
527
  print("[Plot urls]", results['plots'])
528
 
529
  if config.get("enableFineTuning"):
backend/utils/__pycache__/sampling.cpython-310.pyc ADDED
Binary file (3.62 kB). View file
 
backend/utils/__pycache__/utils.cpython-310.pyc ADDED
Binary file (17.5 kB). View file
 
backend/utils/utils.py CHANGED
@@ -9,6 +9,7 @@ import seaborn as sns
9
  import numpy as np
10
  import os
11
  import sys
 
12
  from transformers import (
13
  AutoTokenizer,
14
  AutoModelForCausalLM,
@@ -568,40 +569,97 @@ def _generate_cross_category_cf(base_df, text_col, name_col, category_col, num_c
568
  return augmented_df
569
 
570
  def _ensure_plot_saved(
571
- df,
572
  score_col: str,
573
  basename: str,
574
  group_col: str = None,
575
  target: float = None,
576
  bins: int = 30,
577
  ) -> str:
 
 
 
 
 
 
 
 
578
  os.makedirs("/tmp", exist_ok=True)
579
- path = os.path.join("/tmp", f"{basename}.png")
580
 
581
- plt.figure(figsize=(8, 5))
582
- data = df[score_col].dropna().values
583
 
 
584
  if group_col and group_col in df.columns:
585
- for g, sub in df.groupby(group_col):
586
- vals = sub[score_col].dropna().values
587
- if len(vals) == 0:
588
- continue
589
- plt.hist(vals, bins=bins, alpha=0.4, label=f"{g} (n={len(vals)}, μ={np.mean(vals):.3f})", density=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
590
  else:
591
- plt.hist(data, bins=bins, alpha=0.6, density=True, label=f"All (n={len(data)}, μ={np.mean(data):.3f})")
592
-
593
- if len(data):
594
- m = float(np.mean(data))
595
- plt.axvline(m, linestyle="--", linewidth=2, label=f"mean={m:.3f}")
 
 
 
 
 
 
 
 
 
 
 
596
 
 
597
  if target is not None:
598
- plt.axvline(target, linestyle="-.", linewidth=2, label=f"target={target:.3f}")
599
-
600
- plt.xlabel(score_col)
601
- plt.ylabel("density")
602
- plt.title(basename.replace("_", " "))
603
- plt.legend(loc="best")
604
- plt.tight_layout()
605
- plt.savefig(path, dpi=160)
606
- plt.close()
 
 
 
 
 
 
 
 
 
 
 
 
 
607
  return path
 
9
  import numpy as np
10
  import os
11
  import sys
12
+ import plotly.express as px
13
  from transformers import (
14
  AutoTokenizer,
15
  AutoModelForCausalLM,
 
569
  return augmented_df
570
 
571
  def _ensure_plot_saved(
572
+ df: pd.DataFrame,
573
  score_col: str,
574
  basename: str,
575
  group_col: str = None,
576
  target: float = None,
577
  bins: int = 30,
578
  ) -> str:
579
+ """
580
+ Draw a histogram (density) using Plotly and save as an HTML file.
581
+ - If group_col is provided, colors by group and draws a dashed vertical mean per group.
582
+ - Legend is placed at top-right.
583
+ - Mean value labels are NOT annotated to avoid overlap.
584
+ - Optional target vertical line (dashdot).
585
+ Returns the saved HTML path: /tmp/{basename}.html
586
+ """
587
  os.makedirs("/tmp", exist_ok=True)
588
+ path = os.path.join("/tmp", f"{basename}.html")
589
 
590
+ title = basename.replace("_", " ")
 
591
 
592
+ # Build histogram
593
  if group_col and group_col in df.columns:
594
+ fig = px.histogram(
595
+ df,
596
+ x=score_col,
597
+ color=group_col,
598
+ barmode="overlay",
599
+ nbins=bins,
600
+ opacity=0.6,
601
+ histnorm="probability density",
602
+ title=title,
603
+ )
604
+
605
+ # Group means (no annotations)
606
+ means = (
607
+ df[[group_col, score_col]]
608
+ .dropna(subset=[score_col])
609
+ .groupby(group_col)[score_col]
610
+ .mean()
611
+ )
612
+
613
+ # Map trace colors by trace.name
614
+ color_map = {trace.name: getattr(trace.marker, "color", None) for trace in fig.data}
615
+
616
+ for grp, mean_val in means.items():
617
+ fig.add_vline(
618
+ x=float(mean_val),
619
+ line_width=2,
620
+ line_dash="dash",
621
+ line_color=color_map.get(str(grp), None) # fallback None if missing
622
+ )
623
  else:
624
+ fig = px.histogram(
625
+ df,
626
+ x=score_col,
627
+ nbins=bins,
628
+ opacity=0.6,
629
+ histnorm="probability density",
630
+ title=title,
631
+ )
632
+ # Overall mean (no annotation)
633
+ vals = df[score_col].dropna().values
634
+ if len(vals):
635
+ fig.add_vline(
636
+ x=float(np.mean(vals)),
637
+ line_width=2,
638
+ line_dash="dash",
639
+ )
640
 
641
+ # Optional target line
642
  if target is not None:
643
+ try:
644
+ tval = float(target)
645
+ fig.add_vline(
646
+ x=tval,
647
+ line_width=2,
648
+ line_dash="dashdot", # matplotlib '-.' roughly maps to 'dashdot'
649
+ )
650
+ except (TypeError, ValueError):
651
+ pass
652
+
653
+ # Legend to top-right + right margin for legend
654
+ fig.update_layout(
655
+ legend=dict(
656
+ orientation="v",
657
+ x=0.99, y=0.99,
658
+ xanchor="right", yanchor="top",
659
+ bgcolor="rgba(0,0,0,0)"
660
+ ),
661
+ margin=dict(r=120)
662
+ )
663
+
664
+ fig.write_html(path, include_plotlyjs="cdn")
665
  return path
frontend/public/finetune_aug_generation_result.csv ADDED
The diff for this file is too large to render. See raw diff
 
frontend/public/ft_sentiment_distribution.html ADDED
The diff for this file is too large to render. See raw diff
 
frontend/public/ori_generation_results.csv ADDED
The diff for this file is too large to render. See raw diff
 
frontend/public/ori_sentiment_distribution.html ADDED
The diff for this file is too large to render. See raw diff
 
frontend/src/pages/DatasetConfigPage.tsx CHANGED
@@ -1,5 +1,5 @@
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';
@@ -23,7 +23,7 @@ export default function DatasetConfigPage({ onNext }: Props) {
23
  tau: 0.1,
24
  iterations: 1000,
25
  seed: 42,
26
- enableFineTuning: false, // 需求指出 finetune 不要了,但保留欄位無害
27
  counterfactual: false,
28
  });
29
 
@@ -62,13 +62,22 @@ export default function DatasetConfigPage({ onNext }: Props) {
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 });
@@ -89,6 +98,7 @@ export default function DatasetConfigPage({ onNext }: Props) {
89
  } catch {}
90
  }, []);
91
 
 
92
  useEffect(() => {
93
  setSelectedCfFields([]);
94
  setFieldsError(null);
@@ -96,10 +106,11 @@ export default function DatasetConfigPage({ onNext }: Props) {
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[];
@@ -118,10 +129,9 @@ export default function DatasetConfigPage({ onNext }: Props) {
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([]);
@@ -140,10 +150,13 @@ export default function DatasetConfigPage({ onNext }: Props) {
140
  return () => ac.abort();
141
  }, [cfg.dataset]);
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);
@@ -152,12 +165,23 @@ export default function DatasetConfigPage({ onNext }: Props) {
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({});
@@ -171,7 +195,28 @@ export default function DatasetConfigPage({ onNext }: Props) {
171
  return () => ac.abort();
172
  }, [cfg.dataset, selectedConfig, selectedSplit]);
173
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
  const canNext = !!cfg.dataset;
 
175
 
176
  return (
177
  <div className="space-y-10">
@@ -223,6 +268,37 @@ export default function DatasetConfigPage({ onNext }: Props) {
223
  </label>
224
  ))}
225
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
226
  {/* Custom dataset */}
227
  <label className={choiceRow}>
228
  <input
@@ -285,13 +361,20 @@ export default function DatasetConfigPage({ onNext }: Props) {
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) && (
@@ -341,6 +424,11 @@ export default function DatasetConfigPage({ onNext }: Props) {
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
 
@@ -356,23 +444,31 @@ export default function DatasetConfigPage({ onNext }: Props) {
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}
362
- className="flex items-center gap-2 text-sm text-slate-800 hover:bg-white/60 px-2 py-1 rounded-md transition-colors"
 
 
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"
373
  />
374
  <span>{category}</span>
375
  <span className="text-xs text-slate-500">({count})</span>
 
 
 
376
  </label>
377
  );
378
  })}
@@ -394,7 +490,7 @@ export default function DatasetConfigPage({ onNext }: Props) {
394
  const draft = {
395
  ...cfg,
396
  selectedCfFields,
397
- numCounterfactuals,
398
  // 保留使用者選的 meta config / split
399
  datasetConfig: selectedConfig,
400
  datasetSplit: selectedSplit,
 
1
  import { useEffect, useState } from 'react';
2
+ import { Database, ExternalLink, Shuffle, Lock } from 'lucide-react';
3
  import DatasetValidator from '../components/validators/DatasetValidator';
4
  import { DATASETS } from '../constants/datasets';
5
  import type { JobConfig } from '../types';
 
23
  tau: 0.1,
24
  iterations: 1000,
25
  seed: 42,
26
+ enableFineTuning: false,
27
  counterfactual: false,
28
  });
29
 
 
62
  'bg-white/60 hover:bg-white/80 border-slate-200/60 hover:border-indigo-300';
63
 
64
  const API_BASE = '/api';
65
+
66
+ // 將 example 映射到實際要讀的 Hugging Face 資料集
67
+ function resolveDatasetId(id: string | null | undefined) {
68
+ if (!id) return id;
69
+ return id === 'example' ? 'AmazonScience/bold' : id;
70
+ }
71
+
72
  function buildFieldsURL(datasetId: string, config: string | null, split: string): string {
73
+ const realId = resolveDatasetId(datasetId)!;
74
  const params = new URLSearchParams();
75
+ params.set('id', realId);
76
  if (config && config.trim() !== '') params.set('config', config);
77
  if (split && split.trim() !== '') params.set('split', split);
78
  return `/dataset/fields?${params.toString()}`;
79
  }
80
+
81
  async function fetchJSON<T>(url: string, signal?: AbortSignal): Promise<T> {
82
  const fullURL = url.startsWith('http') ? url : `${API_BASE}${url}`;
83
  const res = await fetch(fullURL, { signal });
 
98
  } catch {}
99
  }, []);
100
 
101
+ // 當 dataset 改變時重新抓取 meta 與 fields
102
  useEffect(() => {
103
  setSelectedCfFields([]);
104
  setFieldsError(null);
 
106
  if (!cfg.dataset || cfg.dataset === 'custom') return;
107
 
108
  const ac = new AbortController();
109
+ const realId = resolveDatasetId(cfg.dataset)!;
110
 
111
  const run = async () => {
112
  try {
113
+ const metaURL = `/dataset/meta?id=${encodeURIComponent(realId)}`; // 用映射後的 id
114
  const meta = await fetchJSON<{
115
  datasetId: string;
116
  configs: string[];
 
129
  setSelectedSplit(defaultSplit);
130
 
131
  setIsLoadingFields(true);
132
+ const fieldsURL = buildFieldsURL(cfg.dataset, defaultConfig, defaultSplit); // 內部會映射
133
+ await fetchJSON<{ fields: string[] }>(fieldsURL, ac.signal);
134
 
 
135
  setFieldsError(null);
136
  } catch (err: any) {
137
  setMetaConfigs([]);
 
150
  return () => ac.abort();
151
  }, [cfg.dataset]);
152
 
153
+ // 當 config/split 改變時,抓 fields 與統計
154
  useEffect(() => {
155
  if (!cfg.dataset || cfg.dataset === 'custom') return;
156
 
157
  const ac = new AbortController();
158
+ const realId = resolveDatasetId(cfg.dataset)!;
159
+
160
  const run = async () => {
161
  try {
162
  setIsLoadingFields(true);
 
165
  const fieldsURL = buildFieldsURL(cfg.dataset, selectedConfig, selectedSplit);
166
  await fetchJSON<{ fields: string[] }>(fieldsURL, ac.signal);
167
 
168
+ // 使用 domain/category 統計
169
+ const statsURL = `/dataset/field-stats?id=${encodeURIComponent(realId)}&field=domain&subfield=category`;
170
  const statsData = await fetchJSON<{ counts: Record<string, Record<string, number>> }>(statsURL, ac.signal);
171
+
172
+ setFieldStats(statsData.counts || {});
173
  setFieldsError(null);
174
+
175
+ // 如果是 example,自動勾選兩個類別
176
+ if (cfg.dataset === 'example') {
177
+ const keys: string[] = [];
178
+ const cats = statsData?.counts?.domain || {};
179
+ if ('American_actors' in cats) keys.push('domain/American_actors');
180
+ if ('American_actresses' in cats) keys.push('domain/American_actresses');
181
+ setSelectedCfFields(keys);
182
+ } else {
183
+ setSelectedCfFields([]);
184
+ }
185
  } catch (err: any) {
186
  const fieldsURL = buildFieldsURL(cfg.dataset, selectedConfig, selectedSplit);
187
  setFieldStats({});
 
195
  return () => ac.abort();
196
  }, [cfg.dataset, selectedConfig, selectedSplit]);
197
 
198
+ // 👉 Example: 自動設定 Counterfactual 數量
199
+ useEffect(() => {
200
+ if (cfg.dataset === 'example') {
201
+ setNumCounterfactuals(20);
202
+ }
203
+ }, [cfg.dataset]);
204
+
205
+ // (保留)當 fieldStats 更新時,如果是 example,自動填入兩個 key
206
+ useEffect(() => {
207
+ if (cfg.dataset !== 'example') return;
208
+ const targets = new Set(['American_actors', 'American_actresses']);
209
+ const keys: string[] = [];
210
+ Object.entries(fieldStats).forEach(([domain, categories]) => {
211
+ Object.keys(categories).forEach((cat) => {
212
+ if (targets.has(cat)) keys.push(`${domain}/${cat}`);
213
+ });
214
+ });
215
+ if (keys.length > 0) setSelectedCfFields(keys);
216
+ }, [cfg.dataset, fieldStats]);
217
+
218
  const canNext = !!cfg.dataset;
219
+ const isExample = cfg.dataset === 'example';
220
 
221
  return (
222
  <div className="space-y-10">
 
268
  </label>
269
  ))}
270
 
271
+ {/* Example dataset (preconfigured) */}
272
+ <label className={choiceRow}>
273
+ <input
274
+ type="radio"
275
+ name="dataset"
276
+ value="example"
277
+ checked={cfg.dataset === 'example'}
278
+ onChange={(e) => {
279
+ setField('dataset', e.target.value);
280
+ setShowCustomDatasetInput(false);
281
+ setCustomDataset('');
282
+ setSelectedCfFields([]); // 將由 effect 根據 fieldStats 自動填
283
+ setNumCounterfactuals(20);
284
+ }}
285
+ className="mt-1 accent-violet-600"
286
+ />
287
+ <div className="flex-1">
288
+ <div className="font-semibold text-slate-900 flex items-center gap-2">
289
+ 🧪 Example
290
+ {isExample && (
291
+ <span className="inline-flex items-center gap-1 text-[10px] font-semibold px-2 py-0.5 rounded-full bg-slate-900 text-white">
292
+ <Lock className="w-3 h-3" /> CF fields locked
293
+ </span>
294
+ )}
295
+ </div>
296
+ <div className="text-xs text-slate-600 mt-1">
297
+ Auto-sets counterfactuals to 20 and locks categories to <code>American_actors</code> & <code>American_actresses</code>.
298
+ </div>
299
+ </div>
300
+ </label>
301
+
302
  {/* Custom dataset */}
303
  <label className={choiceRow}>
304
  <input
 
361
  min={1}
362
  max={20}
363
  step={1}
364
+ value={isExample ? 20 : numCounterfactuals}
365
  onChange={(e) => {
366
+ if (isExample) return; // Example 時忽略修改
367
  const v = parseInt(e.target.value || '3', 10);
368
  setNumCounterfactuals(Number.isFinite(v) ? Math.max(1, Math.min(20, v)) : 3);
369
  }}
370
+ disabled={isExample}
371
+ className={fieldInput + (isExample ? ' cursor-not-allowed opacity-80' : '')}
372
  />
373
+ {isExample && (
374
+ <div className="text-[11px] mt-1 text-slate-500 flex items-center gap-1">
375
+ <Lock className="w-3 h-3" /> Locked to 20 for the Example preset.
376
+ </div>
377
+ )}
378
  </div>
379
 
380
  {(metaConfigs.length > 0 || metaSplits.length > 0) && (
 
424
  <div>
425
  <div className="flex items-center justify-between mb-2">
426
  <div className="text-sm font-semibold text-slate-800">Optional fields</div>
427
+ {isExample && (
428
+ <span className="inline-flex items-center gap-1 text-[10px] font-semibold px-2 py-0.5 rounded-full bg-slate-900 text-white">
429
+ <Lock className="w-3 h-3" /> Locked by Example
430
+ </span>
431
+ )}
432
  {isLoadingFields && <span className="text-xs text-slate-500">Loading</span>}
433
  </div>
434
 
 
444
  {Object.entries(categories).map(([category, count]) => {
445
  const fieldKey = `${domain}/${category}`;
446
  const checked = selectedCfFields.includes(fieldKey);
447
+ const locked = isExample && (category === 'American_actors' || category === 'American_actresses');
448
  return (
449
  <label
450
  key={fieldKey}
451
+ className={`flex items-center gap-2 text-sm text-slate-800 px-2 py-1 rounded-md transition-colors ${
452
+ isExample ? 'opacity-80 cursor-not-allowed' : 'hover:bg-white/60'
453
+ }`}
454
  >
455
  <input
456
  type="checkbox"
457
  checked={checked}
458
+ disabled={isExample}
459
+ onChange={() => {
460
+ if (isExample) return; // Example 時不可手動更改
461
  setSelectedCfFields((prev) =>
462
  checked ? prev.filter((x) => x !== fieldKey) : [...prev, fieldKey]
463
+ );
464
+ }}
465
  className="accent-fuchsia-600"
466
  />
467
  <span>{category}</span>
468
  <span className="text-xs text-slate-500">({count})</span>
469
+ {locked && (
470
+ <span className="ml-1 text-[10px] px-1.5 py-0.5 rounded bg-slate-900 text-white">locked</span>
471
+ )}
472
  </label>
473
  );
474
  })}
 
490
  const draft = {
491
  ...cfg,
492
  selectedCfFields,
493
+ numCounterfactuals: isExample ? 20 : numCounterfactuals, // 例項下強制 20
494
  // 保留使用者選的 meta config / split
495
  datasetConfig: selectedConfig,
496
  datasetSplit: selectedSplit,
frontend/src/pages/ModelConfigPage.tsx CHANGED
@@ -1,5 +1,5 @@
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';
@@ -69,6 +69,18 @@ export default function ModelConfigPage({ onRun }: Props) {
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 ' +
@@ -93,18 +105,25 @@ export default function ModelConfigPage({ onRun }: Props) {
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) => (
@@ -115,7 +134,7 @@ export default function ModelConfigPage({ onRun }: Props) {
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)"
@@ -128,9 +147,9 @@ export default function ModelConfigPage({ onRun }: Props) {
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>
@@ -148,10 +167,19 @@ export default function ModelConfigPage({ onRun }: Props) {
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>
@@ -178,6 +206,11 @@ export default function ModelConfigPage({ onRun }: Props) {
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">
@@ -186,9 +219,13 @@ export default function ModelConfigPage({ onRun }: Props) {
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>
@@ -198,7 +235,8 @@ export default function ModelConfigPage({ onRun }: Props) {
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
@@ -226,10 +264,19 @@ export default function ModelConfigPage({ onRun }: Props) {
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>
@@ -249,11 +296,21 @@ export default function ModelConfigPage({ onRun }: Props) {
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 }));
@@ -261,7 +318,7 @@ export default function ModelConfigPage({ onRun }: Props) {
261
  onRun(
262
  {
263
  ...mergedCfg,
264
- classificationTask,
265
  toxicityModelChoice,
266
  } as any,
267
  {
 
1
  import { useEffect, useState } from 'react';
2
+ import { Bot, Settings2, Lock } from 'lucide-react';
3
  import ModelValidator from '../components/validators/ModelValidator';
4
  import { LM_MODELS } from '../constants/models';
5
  import type { JobConfig } from '../types';
 
69
  } catch {}
70
  }, []);
71
 
72
+ // 🔒 Example 鎖死規則:偵測到 example 就強制設定並關閉自定義輸入
73
+ const isExample = cfg.dataset === 'example';
74
+ useEffect(() => {
75
+ if (!isExample) return;
76
+ setField('languageModel', 'openai-community/gpt2'); // 後端預設也用這個
77
+ setShowCustomLanguageInput(false);
78
+ setCustomLM('');
79
+ setField('k', 10);
80
+ setClassificationTask('sentiment');
81
+ setField('metrictarget', 0.5);
82
+ }, [isExample]); // 當 dataset 改為/離開 example 時觸發
83
+
84
  const card =
85
  'group relative rounded-2xl p-8 border border-white/30 bg-white/60 backdrop-blur-xl ' +
86
  'shadow-[0_15px_40px_-20px_rgba(30,41,59,0.35)] transition-all duration-300 ' +
 
105
  <Bot className="w-6 h-6 text-white" />
106
  </div>
107
  <h3 className={sectionTitle}>Language Generation Model</h3>
108
+ {isExample && (
109
+ <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">
110
+ <Lock className="w-3 h-3" /> Locked by Example
111
+ </span>
112
+ )}
113
  </div>
114
 
115
  <div className="space-y-8">
116
  <div>
117
  <label className="block text-sm font-semibold text-slate-800 mb-2">Model</label>
118
  <select
119
+ value={isExample ? 'openai-community/gpt2' : cfg.languageModel}
120
  onChange={(e) => {
121
+ if (isExample) return;
122
  setField('languageModel', e.target.value);
123
  setShowCustomLanguageInput(e.target.value === 'custom');
124
  }}
125
+ disabled={isExample}
126
+ className={selectInput + (isExample ? ' cursor-not-allowed opacity-80' : '')}
127
  >
128
  <option value="">Select a Language Model</option>
129
  {LM_MODELS.map((m) => (
 
134
  <option value="custom">🔧 Custom Model Upload from Hugging Face</option>
135
  </select>
136
 
137
+ {!isExample && showCustomLanguageInput && (
138
  <input
139
  type="text"
140
  placeholder="Input Hugging Face Model ID (e.g.: microsoft/DialoGPT-medium)"
 
147
  />
148
  )}
149
 
150
+ {(isExample || customLM || cfg.languageModel) && (
151
  <div className="mt-3">
152
+ <ModelValidator modelId={isExample ? 'openai-community/gpt2' : (customLM || cfg.languageModel)} type="language" />
153
  </div>
154
  )}
155
  </div>
 
167
  type="number"
168
  min={1}
169
  max={20}
170
+ value={isExample ? 10 : cfg.k}
171
+ onChange={(e) => {
172
+ if (isExample) return;
173
+ setField('k', parseInt(e.target.value || '0', 10));
174
+ }}
175
+ disabled={isExample}
176
+ className={fieldInput + (isExample ? ' cursor-not-allowed opacity-80' : '')}
177
  />
178
+ {isExample && (
179
+ <div className="text-[11px] mt-1 text-slate-500 flex items-center gap-1">
180
+ <Lock className="w-3 h-3" /> Locked to 10 for the Example preset.
181
+ </div>
182
+ )}
183
  </div>
184
 
185
  <div>
 
206
  <Settings2 className="w-6 h-6 text-white" />
207
  </div>
208
  <h3 className={sectionTitle}>Feature Extraction Model</h3>
209
+ {isExample && (
210
+ <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">
211
+ <Lock className="w-3 h-3" /> Locked by Example
212
+ </span>
213
+ )}
214
  </div>
215
 
216
  <div className="space-y-8">
 
219
  Task
220
  </label>
221
  <select
222
+ value={isExample ? 'sentiment' : classificationTask}
223
+ onChange={(e) => {
224
+ if (isExample) return;
225
+ setClassificationTask(e.target.value as any);
226
+ }}
227
+ disabled={isExample}
228
+ className={selectInput + (isExample ? ' cursor-not-allowed opacity-80' : '')}
229
  >
230
  <option value="sentiment">Sentiment (0–1, Neutral ≈ 0.5)</option>
231
  <option value="regard">Regard (0–2, Neutral ≈ 1.0)</option>
 
235
  </select>
236
  </div>
237
 
238
+ {/* 只有 toxicity 才需要,example 強制 sentiment 就不顯示這塊 */}
239
+ {classificationTask === 'toxicity' && !isExample && (
240
  <div>
241
  <label className="block text-sm font-semibold text-slate-800 mb-1">
242
  Toxicity Model
 
264
  min={0}
265
  max={2}
266
  step={0.01}
267
+ value={isExample ? 0.5 : cfg.metrictarget}
268
+ onChange={(e) => {
269
+ if (isExample) return;
270
+ setField('metrictarget', parseFloat(e.target.value || '0'));
271
+ }}
272
+ disabled={isExample}
273
+ className={fieldInput + (isExample ? ' cursor-not-allowed opacity-80' : '')}
274
  />
275
+ {isExample && (
276
+ <div className="text-[11px] mt-1 text-slate-500 flex items-center gap-1">
277
+ <Lock className="w-3 h-3" /> Locked to 0.5 for the Example preset.
278
+ </div>
279
+ )}
280
  </div>
281
  </div>
282
  </div>
 
296
  }
297
  } catch {}
298
 
299
+ // 🔒 若為 example,再次保險強制設定
300
+ if (isExample) {
301
+ mergedCfg = {
302
+ ...mergedCfg,
303
+ languageModel: 'openai-community/gpt2',
304
+ k: 10,
305
+ metrictarget: 0.5,
306
+ } as JobConfig;
307
+ }
308
+
309
  // 將目前 model 相關設定也寫回草稿,之後回到本頁仍可帶入
310
  const persist = {
311
  ...mergedCfg,
312
+ classificationTask: isExample ? 'sentiment' : classificationTask,
313
+ toxicityModelChoice, // 不會用到,保留存檔
314
  } as any;
315
  localStorage.setItem('cfgDraft', JSON.stringify(persist));
316
  localStorage.setItem('extrasDraft', JSON.stringify({ datasetLimit }));
 
318
  onRun(
319
  {
320
  ...mergedCfg,
321
+ classificationTask: isExample ? 'sentiment' : classificationTask,
322
  toxicityModelChoice,
323
  } as any,
324
  {
frontend/src/pages/ResultsPage.tsx CHANGED
@@ -1,9 +1,125 @@
1
  import PipelineProgress from '../components/PipelineProgress';
2
  import { useJobRunner } from '../hooks/JobRunnerProvider';
 
3
 
4
  export default function ResultsPage() {
5
  const { result, resp, loading, error, url } = useJobRunner();
6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  if (loading && !resp) {
8
  return (
9
  <div className="space-y-6">
@@ -39,9 +155,6 @@ export default function ResultsPage() {
39
  const m = result.metrics!;
40
  const plots = resp.results.plots;
41
 
42
- const originalSrc = url(plots.original_sentiment);
43
- const cfSrc = url(plots.counterfactual_sentiment);
44
-
45
  const r = resp.results as any;
46
  const links: { label: string; href: string }[] = [];
47
 
@@ -54,14 +167,12 @@ export default function ResultsPage() {
54
  if (r?.cf_sentiment_subset_file) {
55
  links.push({ label: 'CF sentiment subset CSV', href: r.cf_sentiment_subset_file });
56
  }
57
-
58
  if (r?.run_config_files?.markdown) {
59
  links.push({ label: 'Run Config (Markdown)', href: r.run_config_files.markdown });
60
  }
61
  if (r?.run_config_files?.json) {
62
  links.push({ label: 'Run Config (JSON)', href: r.run_config_files.json });
63
  }
64
-
65
  if (r?.finetuned_model_zip) {
66
  links.push({ label: 'Fine-tuned Model (ZIP)', href: r.finetuned_model_zip });
67
  } else if (r?.finetuned_model_dir) {
@@ -92,28 +203,12 @@ export default function ResultsPage() {
92
  <h2 className="text-lg font-semibold mb-4">Distribution</h2>
93
  <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
94
  <figure className="rounded-xl overflow-hidden border bg-white">
95
- <img
96
- src={originalSrc}
97
- alt="Original distribution"
98
- className="w-full h-auto"
99
- loading="lazy"
100
- onError={(e) => {
101
- e.currentTarget.alt = 'Original image loading failed';
102
- }}
103
- />
104
  <figcaption className="p-3 text-sm text-slate-600">Original</figcaption>
105
  </figure>
106
 
107
  <figure className="rounded-xl overflow-hidden border bg-white">
108
- <img
109
- src={cfSrc}
110
- alt="Counterfactual distribution"
111
- className="w-full h-auto"
112
- loading="lazy"
113
- onError={(e) => {
114
- e.currentTarget.alt = 'Counterfactual image loading failed';
115
- }}
116
- />
117
  <figcaption className="p-3 text-sm text-slate-600">Counterfactual Augmented</figcaption>
118
  </figure>
119
  </div>
 
1
  import PipelineProgress from '../components/PipelineProgress';
2
  import { useJobRunner } from '../hooks/JobRunnerProvider';
3
+ import { useMemo } from 'react';
4
 
5
  export default function ResultsPage() {
6
  const { result, resp, loading, error, url } = useJobRunner();
7
 
8
+ // 判斷是否為 example(從前面頁面寫入的 cfgDraft)
9
+ const isExample = useMemo(() => {
10
+ try {
11
+ const draft = localStorage.getItem('cfgDraft');
12
+ if (!draft) return false;
13
+ const parsed = JSON.parse(draft);
14
+ return parsed?.dataset === 'example';
15
+ } catch {
16
+ return false;
17
+ }
18
+ }, []);
19
+
20
+ // ---- Example 模式:不跑程式,直接顯示兩張 Plotly HTML ----
21
+ if (isExample) {
22
+ const oriHtml = 'ori_sentiment_distribution.html';
23
+ const ftHtml = 'ft_sentiment_distribution.html';
24
+ const oriCsv = 'ori_generation_results.csv';
25
+ const ftCsv = 'finetune_aug_generation_result.csv';
26
+
27
+ return (
28
+ <div className="space-y-6">
29
+ <section className="p-6 rounded-2xl border border-white/40 bg-white/70 backdrop-blur">
30
+ <h2 className="text-lg font-semibold mb-2">Example Preview</h2>
31
+ <p className="text-sm text-slate-600">
32
+ This is a fixed example preview. The pipeline was not executed.
33
+ </p>
34
+ </section>
35
+
36
+ <section className="p-6 rounded-2xl border border-white/40 bg-white/70 backdrop-blur">
37
+ <h2 className="text-lg font-semibold mb-4">Distribution</h2>
38
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
39
+ <figure className="rounded-xl overflow-hidden border bg-white">
40
+ <iframe
41
+ src={oriHtml}
42
+ title="Original distribution (Plotly)"
43
+ className="w-full"
44
+ style={{ height: 480, border: '0' }}
45
+ />
46
+ <figcaption className="p-3 text-sm text-slate-600">Original</figcaption>
47
+ </figure>
48
+
49
+ <figure className="rounded-xl overflow-hidden border bg-white">
50
+ <iframe
51
+ src={ftHtml}
52
+ title="Counterfactual distribution (Plotly)"
53
+ className="w-full"
54
+ style={{ height: 480, border: '0' }}
55
+ />
56
+ <figcaption className="p-3 text-sm text-slate-600">Counterfactual Augmented</figcaption>
57
+ </figure>
58
+ </div>
59
+ </section>
60
+
61
+ <section className="p-6 rounded-2xl border border-white/40 bg-white/70 backdrop-blur">
62
+ <h2 className="text-lg font-semibold mb-4">Download Report</h2>
63
+ <ul className="space-y-2">
64
+ <li>
65
+ <a
66
+ className="text-indigo-600 hover:underline"
67
+ href={oriCsv}
68
+ target="_blank"
69
+ rel="noreferrer"
70
+ download
71
+ >
72
+ Original Generation CSV
73
+ </a>
74
+ </li>
75
+ <li>
76
+ <a
77
+ className="text-indigo-600 hover:underline"
78
+ href={ftCsv}
79
+ target="_blank"
80
+ rel="noreferrer"
81
+ download
82
+ >
83
+ Counterfactual / Fine-tuned Generation CSV
84
+ </a>
85
+ </li>
86
+ </ul>
87
+ </section>
88
+ </div>
89
+ );
90
+ }
91
+
92
+ // ---- 原本流程(非 example)----
93
+
94
+ // 判斷是否為 html 檔
95
+ const isHtml = (s: string) => /\.html?(?:$|\?)/i.test(s);
96
+
97
+ // 渲染圖表:根據副檔名決定用 <img> 或 <iframe>
98
+ const renderPlot = (srcRaw: string, title: string, alt: string) => {
99
+ const src = url(srcRaw);
100
+ if (isHtml(srcRaw)) {
101
+ return (
102
+ <iframe
103
+ src={src}
104
+ title={title}
105
+ className="w-full"
106
+ style={{ height: 480, border: '0' }}
107
+ />
108
+ );
109
+ }
110
+ return (
111
+ <img
112
+ src={src}
113
+ alt={alt}
114
+ className="w-full h-auto"
115
+ loading="lazy"
116
+ onError={(e) => {
117
+ e.currentTarget.alt = `${alt} loading failed`;
118
+ }}
119
+ />
120
+ );
121
+ };
122
+
123
  if (loading && !resp) {
124
  return (
125
  <div className="space-y-6">
 
155
  const m = result.metrics!;
156
  const plots = resp.results.plots;
157
 
 
 
 
158
  const r = resp.results as any;
159
  const links: { label: string; href: string }[] = [];
160
 
 
167
  if (r?.cf_sentiment_subset_file) {
168
  links.push({ label: 'CF sentiment subset CSV', href: r.cf_sentiment_subset_file });
169
  }
 
170
  if (r?.run_config_files?.markdown) {
171
  links.push({ label: 'Run Config (Markdown)', href: r.run_config_files.markdown });
172
  }
173
  if (r?.run_config_files?.json) {
174
  links.push({ label: 'Run Config (JSON)', href: r.run_config_files.json });
175
  }
 
176
  if (r?.finetuned_model_zip) {
177
  links.push({ label: 'Fine-tuned Model (ZIP)', href: r.finetuned_model_zip });
178
  } else if (r?.finetuned_model_dir) {
 
203
  <h2 className="text-lg font-semibold mb-4">Distribution</h2>
204
  <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
205
  <figure className="rounded-xl overflow-hidden border bg-white">
206
+ {renderPlot(plots.original_sentiment, 'Original distribution (Plotly)', 'Original distribution')}
 
 
 
 
 
 
 
 
207
  <figcaption className="p-3 text-sm text-slate-600">Original</figcaption>
208
  </figure>
209
 
210
  <figure className="rounded-xl overflow-hidden border bg-white">
211
+ {renderPlot(plots.counterfactual_sentiment, 'Counterfactual distribution (Plotly)', 'Counterfactual distribution')}
 
 
 
 
 
 
 
 
212
  <figcaption className="p-3 text-sm text-slate-600">Counterfactual Augmented</figcaption>
213
  </figure>
214
  </div>