peihsin0715
commited on
Commit
·
ad9612f
1
Parent(s):
444d92d
Add example
Browse files- backend/requirements.txt +2 -1
- backend/server.py +4 -3
- backend/utils/__pycache__/sampling.cpython-310.pyc +0 -0
- backend/utils/__pycache__/utils.cpython-310.pyc +0 -0
- backend/utils/utils.py +81 -23
- frontend/public/finetune_aug_generation_result.csv +0 -0
- frontend/public/ft_sentiment_distribution.html +0 -0
- frontend/public/ori_generation_results.csv +0 -0
- frontend/public/ori_sentiment_distribution.html +0 -0
- frontend/src/pages/DatasetConfigPage.tsx +114 -18
- frontend/src/pages/ModelConfigPage.tsx +76 -19
- frontend/src/pages/ResultsPage.tsx +118 -23
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/{
|
| 523 |
-
'counterfactual_sentiment': f"/tmp/{
|
| 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}.
|
| 580 |
|
| 581 |
-
|
| 582 |
-
data = df[score_col].dropna().values
|
| 583 |
|
|
|
|
| 584 |
if group_col and group_col in df.columns:
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 590 |
else:
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 596 |
|
|
|
|
| 597 |
if target is not None:
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|
| 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',
|
| 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(
|
| 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 |
-
|
| 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 |
-
//
|
| 156 |
-
const statsURL = `/dataset/field-stats?id=${encodeURIComponent(
|
| 157 |
const statsData = await fetchJSON<{ counts: Record<string, Record<string, number>> }>(statsURL, ac.signal);
|
| 158 |
-
|
|
|
|
| 159 |
setFieldsError(null);
|
| 160 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 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=
|
|
|
|
|
|
|
| 363 |
>
|
| 364 |
<input
|
| 365 |
type="checkbox"
|
| 366 |
checked={checked}
|
| 367 |
-
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 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) =>
|
| 153 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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) =>
|
| 191 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
{
|
|
|
|
| 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) =>
|
| 231 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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 |
-
|
| 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>
|