Skip to content

Commit b714b87

Browse files
committed
feat: Add dataset statistics and enhance UI for dataset management and online test setup
1 parent 4cdbd92 commit b714b87

File tree

5 files changed

+338
-92
lines changed

5 files changed

+338
-92
lines changed

app.py

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -250,19 +250,74 @@ def ensure_datasets_dir():
250250
os.makedirs(DATASETS_DIR)
251251

252252
def get_datasets():
253-
"""利用可能なデータセット一覧を取得"""
253+
"""利用可能なデータセット一覧を取得(統計情報付き)"""
254254
ensure_datasets_dir()
255255
datasets = []
256256
for filename in os.listdir(DATASETS_DIR):
257257
if filename.endswith('.csv'):
258258
name = filename[:-4] # .csvを除去
259+
stats = get_dataset_stats(filename)
259260
datasets.append({
260261
'name': name,
261262
'filename': filename,
262-
'path': os.path.join(DATASETS_DIR, filename)
263+
'path': os.path.join(DATASETS_DIR, filename),
264+
'stats': stats
263265
})
264266
return datasets
265267

268+
def get_dataset_stats(data_or_filename):
269+
"""データセットの統計情報を取得(習熟度スコアベース)"""
270+
# データまたはファイル名を受け取り、データを取得
271+
if isinstance(data_or_filename, str):
272+
# ファイル名が渡された場合
273+
data = load_dataset(data_or_filename)
274+
else:
275+
# データリストが直接渡された場合
276+
data = data_or_filename
277+
278+
if not data:
279+
return {
280+
'total_problems': 0,
281+
'average_mastery': 0.0,
282+
'total_attempts': 0,
283+
'total_correct': 0,
284+
'studied_problems': 0
285+
}
286+
287+
total_problems = len(data)
288+
total_attempts = 0
289+
total_correct = 0
290+
mastery_sum = 0.0
291+
studied_problems = 0
292+
293+
for item in data:
294+
try:
295+
correct = int(item.get('正解数', 0) or 0)
296+
attempts = int(item.get('総試行回数', 0) or 0)
297+
mastery_score = float(item.get('習熟度スコア', 0.0) or 0.0)
298+
299+
total_correct += correct
300+
total_attempts += attempts
301+
mastery_sum += mastery_score
302+
303+
# 学習済みの問題をカウント(試行回数が1回以上)
304+
if attempts > 0:
305+
studied_problems += 1
306+
307+
except (ValueError, TypeError):
308+
continue
309+
310+
# 平均習熟度スコアを計算(0-100の範囲で表示)
311+
average_mastery = (mastery_sum / total_problems * 100) if total_problems > 0 else 0.0
312+
313+
return {
314+
'total_problems': total_problems,
315+
'average_mastery': round(average_mastery, 1),
316+
'total_attempts': total_attempts,
317+
'total_correct': total_correct,
318+
'studied_problems': studied_problems
319+
}
320+
266321
def load_dataset(filename):
267322
"""CSVファイルからデータセットを読み込み(習熟度データ対応)"""
268323
filepath = os.path.join(DATASETS_DIR, filename)
@@ -384,11 +439,17 @@ def save_dataset(filename, data, fieldnames=None):
384439

385440
@app.route('/')
386441
def index():
387-
"""メインページ"""
442+
"""メインダッシュボードページ"""
388443
datasets = get_datasets()
389444
message, message_type = get_message_and_type(request)
390445
return render_template('index.html', datasets=datasets, message=message, message_type=message_type)
391446

447+
@app.route('/api/datasets')
448+
def api_datasets():
449+
"""データセット一覧API(AJAX用)"""
450+
datasets = get_datasets()
451+
return jsonify(datasets)
452+
392453
@app.route('/create_dataset')
393454
def create_dataset():
394455
"""新しいデータセット作成ページ"""
@@ -428,6 +489,9 @@ def edit_dataset(filename):
428489
data = load_dataset(filename)
429490
dataset_name = filename[:-4] # .csvを除去
430491

492+
# 統計情報を取得
493+
stats = get_dataset_stats(data)
494+
431495
# 拡張フォーマット: 番号,質問,回答,正解数,総試行回数,習熟度スコア
432496
fieldnames = ['番号', '質問', '回答', '正解数', '総試行回数', '習熟度スコア']
433497

@@ -437,6 +501,7 @@ def edit_dataset(filename):
437501
dataset_name=dataset_name,
438502
filename=filename,
439503
data=data,
504+
stats=stats,
440505
fieldnames=fieldnames,
441506
message=message,
442507
message_type=message_type)

templates/base.html

Lines changed: 61 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,36 @@
2525
background-color: #198754;
2626
border-color: #198754;
2727
}
28+
.btn-info {
29+
background-color: #b19cd9;
30+
border-color: #b19cd9;
31+
}
32+
.btn-info:hover {
33+
background-color: #9b59b6;
34+
border-color: #9b59b6;
35+
}
2836
.btn-danger {
2937
background-color: #dc3545;
3038
border-color: #dc3545;
3139
}
3240
.dataset-card {
33-
transition: transform 0.2s;
41+
transition: transform 0.2s, box-shadow 0.2s;
3442
}
3543
.dataset-card:hover {
3644
transform: translateY(-2px);
3745
}
46+
.progress {
47+
background-color: #e9ecef;
48+
}
49+
.card-header.bg-primary {
50+
background: linear-gradient(45deg, #0d6efd, #0056b3) !important;
51+
}
52+
.card-header.bg-dark {
53+
background: linear-gradient(45deg, #343a40, #212529) !important;
54+
}
55+
.border-end {
56+
border-right: 1px solid #dee2e6 !important;
57+
}
3858
</style>
3959
</head>
4060
<body>
@@ -47,20 +67,20 @@
4767
<span class="navbar-toggler-icon"></span>
4868
</button>
4969
<div class="collapse navbar-collapse" id="navbarNav">
50-
<ul class="navbar-nav ms-auto">
51-
<li class="nav-item">
52-
<a class="nav-link" href="{{ url_for('index') }}">
53-
<i class="fas fa-home"></i> ホーム
54-
</a>
55-
</li>
56-
<li class="nav-item">
57-
<a class="nav-link" href="{{ url_for('create_dataset') }}">
58-
<i class="fas fa-plus"></i> 新しいデータセット
70+
<ul class="navbar-nav me-auto">
71+
<li class="nav-item dropdown">
72+
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
73+
<i class="fas fa-database"></i> データセット一覧
5974
</a>
75+
<ul class="dropdown-menu" aria-labelledby="navbarDropdown" id="dataset-dropdown">
76+
<li><a class="dropdown-item text-muted"><i class="fas fa-spinner fa-spin"></i> 読み込み中...</a></li>
77+
</ul>
6078
</li>
79+
</ul>
80+
<ul class="navbar-nav ms-auto">
6181
<li class="nav-item">
62-
<a class="nav-link" href="{{ url_for('import_dataset_page') }}">
63-
<i class="fas fa-upload"></i> インポート
82+
<a class="nav-link" href="{{ url_for('index') }}">
83+
<i class="fas fa-tachometer-alt"></i> ダッシュボード
6484
</a>
6585
</li>
6686
</ul>
@@ -85,6 +105,35 @@
85105
</div>
86106

87107
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
108+
<script>
109+
// データセット一覧をナビゲーションメニューに動的読み込み
110+
document.addEventListener('DOMContentLoaded', function() {
111+
const dropdown = document.getElementById('dataset-dropdown');
112+
if (dropdown) {
113+
fetch('/api/datasets')
114+
.then(response => response.json())
115+
.then(datasets => {
116+
dropdown.innerHTML = '';
117+
if (datasets.length === 0) {
118+
dropdown.innerHTML = '<li><a class="dropdown-item text-muted"><i class="fas fa-info-circle"></i> データセットがありません</a></li>';
119+
} else {
120+
datasets.forEach(dataset => {
121+
const item = document.createElement('li');
122+
item.innerHTML = `<a class="dropdown-item" href="/edit_dataset/${dataset.filename}">
123+
<i class="fas fa-database me-2"></i>${dataset.name}
124+
<small class="text-muted ms-2">(${dataset.stats.total_problems}問)</small>
125+
</a>`;
126+
dropdown.appendChild(item);
127+
});
128+
}
129+
})
130+
.catch(error => {
131+
console.error('データセット一覧の取得に失敗しました:', error);
132+
dropdown.innerHTML = '<li><a class="dropdown-item text-danger"><i class="fas fa-exclamation-triangle"></i> 読み込みエラー</a></li>';
133+
});
134+
}
135+
});
136+
</script>
88137
{% block scripts %}{% endblock %}
89138
</body>
90139
</html>

templates/edit_dataset.html

Lines changed: 79 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,81 @@
88
<div class="d-flex justify-content-between align-items-center mb-4">
99
<h1><i class="fas fa-edit"></i> {{ dataset_name }}</h1>
1010
<div>
11-
<a href="{{ url_for('export_dataset', filename=filename) }}" class="btn btn-info me-2">
12-
<i class="fas fa-download"></i> エクスポート
11+
<a href="{{ url_for('export_dataset', filename=filename) }}" class="btn btn-outline-info me-2">
12+
<i class="fas fa-download"></i> 出力
1313
</a>
14+
<button type="button" class="btn btn-outline-danger me-2"
15+
onclick="confirmDeleteDataset('{{ dataset_name }}', '{{ filename }}')">
16+
<i class="fas fa-trash"></i> データセット削除
17+
</button>
1418
<a href="{{ url_for('index') }}" class="btn btn-secondary">
1519
<i class="fas fa-arrow-left"></i> 戻る
1620
</a>
1721
</div>
1822
</div>
23+
24+
<!-- 統計情報カード -->
25+
<div class="card mb-4 shadow-sm">
26+
<div class="card-header bg-info text-white">
27+
<h5 class="mb-0"><i class="fas fa-chart-bar me-2"></i>データセット統計</h5>
28+
</div>
29+
<div class="card-body">
30+
<div class="row text-center">
31+
<div class="col-md-3 col-6">
32+
<div class="border-end border-md-end-0 border-md-bottom mb-md-3">
33+
<h3 class="text-primary mb-1">{{ stats.total_problems }}</h3>
34+
<small class="text-muted">総問題数</small>
35+
</div>
36+
</div>
37+
<div class="col-md-3 col-6">
38+
<div class="border-md-end border-md-bottom mb-md-3">
39+
<h3 class="mb-1 {% if stats.average_mastery >= 80 %}text-success{% elif stats.average_mastery >= 60 %}text-warning{% else %}text-danger{% endif %}">
40+
{{ stats.average_mastery }}%
41+
</h3>
42+
<small class="text-muted">平均習熟度</small>
43+
</div>
44+
</div>
45+
<div class="col-md-3 col-6">
46+
<div class="border-end border-md-end-0 mt-3 mt-md-0">
47+
<h3 class="text-success mb-1">{{ stats.studied_problems }}</h3>
48+
<small class="text-muted">学習済み問題</small>
49+
</div>
50+
</div>
51+
<div class="col-md-3 col-6">
52+
<div class="mt-3 mt-md-0">
53+
<h3 class="text-info mb-1">{{ stats.total_attempts }}</h3>
54+
<small class="text-muted">総試行回数</small>
55+
</div>
56+
</div>
57+
</div>
58+
59+
<!-- 習熟度プログレスバー -->
60+
{% if stats.total_attempts > 0 %}
61+
<div class="mt-3">
62+
<div class="d-flex justify-content-between align-items-center mb-2">
63+
<span class="small text-muted">学習進捗</span>
64+
<span class="small text-muted">{{ stats.studied_problems }}/{{ stats.total_problems }} 問完了</span>
65+
</div>
66+
<div class="progress" style="height: 10px;">
67+
<div class="progress-bar
68+
{% if stats.average_mastery >= 80 %}bg-success
69+
{% elif stats.average_mastery >= 60 %}bg-warning
70+
{% else %}bg-danger{% endif %}"
71+
role="progressbar"
72+
data-width="{{ stats.average_mastery }}"
73+
aria-valuenow="{{ stats.average_mastery }}"
74+
aria-valuemin="0"
75+
aria-valuemax="100">
76+
</div>
77+
</div>
78+
</div>
79+
{% else %}
80+
<div class="mt-3 text-center">
81+
<p class="text-muted mb-0"><i class="fas fa-info-circle me-2"></i>まだ学習が開始されていません</p>
82+
</div>
83+
{% endif %}
84+
</div>
85+
</div>
1986
</div>
2087
</div>
2188

@@ -65,7 +132,7 @@ <h6><i class="fas fa-file-csv"></i> CSV直接編集</h6>
65132
<h5><i class="fas fa-list"></i> 登録済みアイテム ({{ data|length }}件)</h5>
66133
{% if data %}
67134
<div class="btn-group" role="group">
68-
<a href="{{ url_for('online_test_setup', filename=filename) }}" class="btn btn-primary btn-sm">
135+
<a href="{{ url_for('online_test_setup', filename=filename) }}" class="btn btn-info btn-sm">
69136
<i class="fas fa-laptop"></i> オンラインテスト
70137
</a>
71138
<a href="{{ url_for('generate_quiz', filename=filename) }}" class="btn btn-success btn-sm">
@@ -142,7 +209,7 @@ <h5><i class="fas fa-list"></i> 登録済みアイテム ({{ data|length }}件)<
142209
{% block scripts %}
143210
<script>
144211
document.addEventListener('DOMContentLoaded', function() {
145-
// プログレスバーの幅を設定
212+
// すべてのプログレスバーの幅を設定(統計情報とテーブル内の両方)
146213
const progressBars = document.querySelectorAll('.progress-bar[data-width]');
147214
progressBars.forEach(function(bar) {
148215
const width = bar.getAttribute('data-width');
@@ -164,5 +231,13 @@ <h5><i class="fas fa-list"></i> 登録済みアイテム ({{ data|length }}件)<
164231
}
165232
}
166233
});
234+
235+
// データセット削除確認ダイアログ
236+
function confirmDeleteDataset(datasetName, filename) {
237+
if (confirm(`データセット「${datasetName}」を完全に削除しますか?\n\nこの操作は取り消せません。すべての学習履歴も失われます。`)) {
238+
// 削除実行
239+
window.location.href = `{{ url_for('delete_dataset', filename='') }}${filename}`;
240+
}
241+
}
167242
</script>
168243
{% endblock %}

0 commit comments

Comments
 (0)