davanstrien HF Staff Claude commited on
Commit
a21dde3
·
1 Parent(s): 656300e

Improve UI with Tufte-inspired design principles

Browse files

- Add model documentation and links to davanstrien/iconclass-vlm
- Simplify visual design: remove shadows, minimize borders, reduce clutter
- Make raw predictions collapsible (hidden by default)
- Switch to single-column layout for focused evaluation
- Improve typography: consistent sizing, lighter secondary text
- Minimize color palette: only grays and subtle green for matches
- Add CLAUDE.md documentation file

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>

Files changed (2) hide show
  1. CLAUDE.md +46 -0
  2. index.html +156 -123
CLAUDE.md ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ This is a Hugging Face Space that visualizes ICONCLASS predictions from the `davanstrien/iconclass-vlm` model compared to ground truth labels. It's a static web application that fetches and displays data from the `davanstrien/iconclass-sft-predictions` dataset.
8
+
9
+ The model being evaluated is a fine-tuned vision-language model (based on Qwen/Qwen2.5-VL-3B-Instruct) that automatically classifies art and cultural heritage images using Iconclass notation. The visualization shows how well the model's predictions match the ground truth labels.
10
+
11
+ ## Architecture
12
+
13
+ The project consists of a single-page web application:
14
+ - `index.html`: Main application with embedded CSS and JavaScript
15
+ - `style.css`: Additional styles (currently minimal, most styles are inline in index.html)
16
+ - Uses the Hugging Face Datasets Server API to fetch data
17
+
18
+ ## Key Implementation Details
19
+
20
+ ### Data Source
21
+ - Dataset: `davanstrien/iconclass-sft-predictions`
22
+ - Config: `default`
23
+ - Split: `test`
24
+ - API endpoint: `https://datasets-server.huggingface.co/rows`
25
+
26
+ ### Data Structure
27
+ Each row contains:
28
+ - `images`: Array of image objects with `src` URLs
29
+ - `iconclass-prediction`: Raw prediction text
30
+ - `iconclass-predictions-parsed`: Parsed prediction labels array
31
+ - `iconclass-gt-parsed`: Parsed ground truth labels array
32
+
33
+ ### Core Functionality
34
+ - Lazy loading with pagination (10 images at a time)
35
+ - Infinite scroll support
36
+ - Visual comparison between predictions and ground truth
37
+ - Match detection and scoring
38
+ - Invalid label detection (labels containing "not a valid" or "invalid")
39
+
40
+ ## Development Notes
41
+
42
+ Since this is a static Hugging Face Space (sdk: static), there's no build process or backend. All changes are made directly to the HTML/CSS files and are immediately reflected when deployed to Hugging Face Spaces.
43
+
44
+ **Important**: The README.md file is not displayed in static Spaces. Any documentation or description about the Space must be added directly to the `index.html` file to be visible to users.
45
+
46
+ To test locally, simply open `index.html` in a web browser.
index.html CHANGED
@@ -2,7 +2,8 @@
2
  <html>
3
  <head>
4
  <meta charset="UTF-8" />
5
- <title>ICONCLASS Dataset Viewer</title>
 
6
  <style>
7
  * {
8
  margin: 0;
@@ -13,77 +14,120 @@
13
  body {
14
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
15
  sans-serif;
16
- background: #f9f9f9;
17
  padding: 20px;
18
- line-height: 1.6;
 
 
19
  }
20
 
21
  .header {
22
- text-align: center;
23
- margin-bottom: 30px;
24
- padding: 20px;
 
 
25
  }
26
 
27
  h1 {
28
- font-size: 24px;
29
  font-weight: 600;
30
  color: #333;
31
- margin-bottom: 10px;
32
  }
33
 
34
- .stats {
35
  font-size: 14px;
36
  color: #666;
 
 
37
  }
38
 
39
- .gallery {
40
- display: grid;
41
- grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
42
- gap: 30px;
43
- max-width: 1200px;
44
- margin: 0 auto;
45
  }
46
 
47
- @media (max-width: 600px) {
48
- .gallery {
49
- grid-template-columns: 1fr;
50
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  }
52
 
53
  .card {
54
  background: white;
55
- border-radius: 12px;
 
56
  overflow: hidden;
57
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
58
  }
59
 
60
  .card img {
61
  width: 100%;
62
  height: auto;
 
 
63
  display: block;
64
- border-bottom: 1px solid #eee;
 
65
  }
66
 
67
  .card-content {
68
- padding: 20px;
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  }
70
 
71
  .raw-prediction {
72
- background: #f5f5f5;
73
- padding: 8px 12px;
74
- border-radius: 6px;
 
 
75
  font-family: "SF Mono", Monaco, "Cascadia Code", monospace;
76
- font-size: 12px;
77
  color: #666;
78
- margin-bottom: 20px;
79
  word-break: break-all;
 
 
 
 
 
80
  }
81
 
82
  .comparison {
83
  display: grid;
84
  grid-template-columns: 1fr 1fr;
85
  gap: 20px;
86
- margin-top: 15px;
87
  }
88
 
89
  .column {
@@ -91,147 +135,120 @@
91
  }
92
 
93
  .column-title {
94
- font-weight: 600;
95
  margin-bottom: 8px;
96
- color: #333;
97
- font-size: 11px;
98
- text-transform: uppercase;
99
  letter-spacing: 0.5px;
 
100
  }
101
 
102
  .label {
103
- padding: 6px 10px;
104
- margin: 4px 0;
105
- border-radius: 6px;
106
  position: relative;
107
- transition: all 0.2s ease;
 
 
 
 
 
 
108
  }
109
 
110
  /* Prediction styles */
111
  .prediction {
112
- background: #f0f4ff;
113
- border: 1px solid #d0deff;
114
  }
115
 
116
  .prediction.invalid {
117
- background: #fff0f0;
118
- border: 1px solid #ffcccc;
119
- color: #cc0000;
120
  text-decoration: line-through;
121
- opacity: 0.7;
122
  }
123
 
124
  .prediction.match {
125
- background: #e8f5e9;
126
- border: 1px solid #a5d6a7;
127
- }
128
-
129
- .prediction.match::before {
130
- content: "✓";
131
- position: absolute;
132
- left: -20px;
133
- color: #4caf50;
134
- font-weight: bold;
135
  }
136
 
137
  /* Ground truth styles */
138
  .ground-truth {
139
- background: #f5f5f5;
140
- border: 1px solid #e0e0e0;
141
  }
142
 
143
  .ground-truth.matched {
144
- background: #e8f5e9;
145
- border: 1px solid #a5d6a7;
146
- }
147
-
148
- /* Match indicator */
149
- .match-indicator {
150
- display: inline-block;
151
- margin-left: 8px;
152
- padding: 2px 6px;
153
- background: #4caf50;
154
- color: white;
155
- border-radius: 10px;
156
- font-size: 10px;
157
- font-weight: 600;
158
  }
159
 
160
  .controls {
161
  text-align: center;
162
- margin: 40px 0;
 
163
  }
164
 
165
- button {
166
- background: #333;
167
- color: white;
168
- border: none;
169
- padding: 12px 30px;
170
- border-radius: 8px;
171
- font-size: 14px;
172
  cursor: pointer;
173
- transition: all 0.2s ease;
174
- font-weight: 500;
 
 
175
  }
176
 
177
- button:hover {
178
- background: #555;
179
- transform: translateY(-1px);
180
  }
181
 
182
- button:disabled {
183
- background: #ddd;
184
- cursor: not-allowed;
185
- transform: none;
186
  }
187
 
188
  .loading {
189
  text-align: center;
190
- padding: 20px;
191
- color: #666;
192
- font-size: 14px;
193
  }
194
 
195
  .loading.hidden {
196
  display: none;
197
  }
198
-
199
- .score-badge {
200
- display: inline-block;
201
- margin-top: 10px;
202
- padding: 4px 12px;
203
- background: #f0f0f0;
204
- border-radius: 20px;
205
- font-size: 12px;
206
- color: #666;
207
- }
208
-
209
- .score-badge.good {
210
- background: #e8f5e9;
211
- color: #2e7d32;
212
- }
213
-
214
- .score-badge.poor {
215
- background: #fff3e0;
216
- color: #e65100;
217
- }
218
  </style>
219
  </head>
220
  <body>
221
  <div class="header">
222
- <h1>ICONCLASS Predictions vs Ground Truth</h1>
 
 
 
 
 
 
223
  <div class="stats">
224
- <span id="loadedCount">0</span> / <span id="totalCount">-</span> images
225
- loaded
226
  </div>
227
  </div>
228
 
229
  <div id="gallery" class="gallery"></div>
230
 
231
- <div class="loading hidden" id="loading">Loading images...</div>
232
 
233
  <div class="controls">
234
- <button id="loadMore">Load More</button>
235
  </div>
236
 
237
  <script>
@@ -283,7 +300,7 @@
283
  loadMoreBtn.disabled = true;
284
  loadMoreBtn.textContent = "All Images Loaded";
285
  } else {
286
- loadMoreBtn.textContent = `Load More (${
287
  totalRows - currentOffset
288
  } remaining)`;
289
  }
@@ -317,11 +334,27 @@
317
  const content = document.createElement("div");
318
  content.className = "card-content";
319
 
320
- // Show raw prediction
321
  if (row["iconclass-prediction"]) {
 
 
 
 
322
  const rawDiv = document.createElement("div");
323
  rawDiv.className = "raw-prediction";
324
  rawDiv.textContent = row["iconclass-prediction"];
 
 
 
 
 
 
 
 
 
 
 
 
325
  content.appendChild(rawDiv);
326
  }
327
 
@@ -399,7 +432,7 @@
399
  comparison.appendChild(gtColumn);
400
  content.appendChild(comparison);
401
 
402
- // Add match score
403
  const validPredictions = predictions.filter(
404
  (_, idx) => !invalidPredictions[idx]
405
  );
@@ -408,12 +441,12 @@
408
  ? Math.round((matches.length / validPredictions.length) * 100)
409
  : 0;
410
 
411
- const scoreBadge = document.createElement("div");
412
- scoreBadge.className = `score-badge ${
413
- matchScore > 50 ? "good" : matchScore > 0 ? "poor" : ""
414
- }`;
415
- scoreBadge.textContent = `${matches.length}/${validPredictions.length} matches (${matchScore}%)`;
416
- content.appendChild(scoreBadge);
417
 
418
  card.appendChild(content);
419
  gallery.appendChild(card);
 
2
  <html>
3
  <head>
4
  <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>ICONCLASS Model Evaluation - davanstrien/iconclass-vlm</title>
7
  <style>
8
  * {
9
  margin: 0;
 
14
  body {
15
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
16
  sans-serif;
17
+ background: white;
18
  padding: 20px;
19
+ line-height: 1.5;
20
+ color: #333;
21
+ font-size: 14px;
22
  }
23
 
24
  .header {
25
+ max-width: 800px;
26
+ margin: 0 auto 30px;
27
+ padding: 0;
28
+ border-bottom: 1px solid #e5e5e5;
29
+ padding-bottom: 20px;
30
  }
31
 
32
  h1 {
33
+ font-size: 20px;
34
  font-weight: 600;
35
  color: #333;
36
+ margin: 0 0 8px 0;
37
  }
38
 
39
+ .subtitle {
40
  font-size: 14px;
41
  color: #666;
42
+ margin: 0 0 12px 0;
43
+ line-height: 1.4;
44
  }
45
 
46
+ .subtitle a {
47
+ color: #0066cc;
48
+ text-decoration: none;
 
 
 
49
  }
50
 
51
+ .subtitle a:hover {
52
+ text-decoration: underline;
53
+ }
54
+
55
+ .description {
56
+ font-size: 13px;
57
+ color: #666;
58
+ margin: 0 0 8px 0;
59
+ line-height: 1.5;
60
+ }
61
+
62
+ .stats {
63
+ font-size: 11px;
64
+ color: #999;
65
+ margin: 0;
66
+ }
67
+
68
+ .gallery {
69
+ max-width: 800px;
70
+ margin: 0 auto;
71
  }
72
 
73
  .card {
74
  background: white;
75
+ border: 1px solid #e5e5e5;
76
+ border-radius: 2px;
77
  overflow: hidden;
78
+ margin-bottom: 20px;
79
  }
80
 
81
  .card img {
82
  width: 100%;
83
  height: auto;
84
+ max-height: 500px;
85
+ object-fit: contain;
86
  display: block;
87
+ border-bottom: 1px solid #e5e5e5;
88
+ background: #fafafa;
89
  }
90
 
91
  .card-content {
92
+ padding: 15px;
93
+ }
94
+
95
+ .raw-toggle {
96
+ font-size: 11px;
97
+ color: #999;
98
+ cursor: pointer;
99
+ margin-bottom: 12px;
100
+ user-select: none;
101
+ font-family: "SF Mono", Monaco, "Cascadia Code", monospace;
102
+ }
103
+
104
+ .raw-toggle:hover {
105
+ color: #666;
106
  }
107
 
108
  .raw-prediction {
109
+ display: none;
110
+ background: #fafafa;
111
+ padding: 8px;
112
+ border: 1px solid #e5e5e5;
113
+ border-radius: 2px;
114
  font-family: "SF Mono", Monaco, "Cascadia Code", monospace;
115
+ font-size: 11px;
116
  color: #666;
117
+ margin-bottom: 15px;
118
  word-break: break-all;
119
+ line-height: 1.4;
120
+ }
121
+
122
+ .raw-prediction.visible {
123
+ display: block;
124
  }
125
 
126
  .comparison {
127
  display: grid;
128
  grid-template-columns: 1fr 1fr;
129
  gap: 20px;
130
+ margin-top: 0;
131
  }
132
 
133
  .column {
 
135
  }
136
 
137
  .column-title {
138
+ font-weight: 400;
139
  margin-bottom: 8px;
140
+ color: #999;
141
+ font-size: 10px;
 
142
  letter-spacing: 0.5px;
143
+ text-transform: uppercase;
144
  }
145
 
146
  .label {
147
+ padding: 3px 0 3px 6px;
148
+ margin: 2px 0;
 
149
  position: relative;
150
+ border-left: 2px solid transparent;
151
+ font-size: 13px;
152
+ line-height: 1.4;
153
+ }
154
+
155
+ .label:hover {
156
+ background: #fafafa;
157
  }
158
 
159
  /* Prediction styles */
160
  .prediction {
161
+ color: #333;
 
162
  }
163
 
164
  .prediction.invalid {
165
+ color: #999;
 
 
166
  text-decoration: line-through;
 
167
  }
168
 
169
  .prediction.match {
170
+ border-left-color: #4caf50;
171
+ font-weight: 500;
 
 
 
 
 
 
 
 
172
  }
173
 
174
  /* Ground truth styles */
175
  .ground-truth {
176
+ color: #333;
 
177
  }
178
 
179
  .ground-truth.matched {
180
+ border-left-color: #4caf50;
181
+ font-weight: 500;
182
+ }
183
+
184
+ /* Match statistics */
185
+ .match-stats {
186
+ font-size: 11px;
187
+ color: #999;
188
+ margin-top: 10px;
189
+ padding-top: 10px;
190
+ border-top: 1px solid #f0f0f0;
 
 
 
191
  }
192
 
193
  .controls {
194
  text-align: center;
195
+ margin: 30px 0;
196
+ padding-top: 20px;
197
  }
198
 
199
+ .load-more {
200
+ color: #0066cc;
201
+ text-decoration: none;
202
+ font-size: 13px;
 
 
 
203
  cursor: pointer;
204
+ background: none;
205
+ border: none;
206
+ padding: 0;
207
+ font-family: inherit;
208
  }
209
 
210
+ .load-more:hover {
211
+ text-decoration: underline;
 
212
  }
213
 
214
+ .load-more:disabled {
215
+ color: #999;
216
+ cursor: default;
217
+ text-decoration: none;
218
  }
219
 
220
  .loading {
221
  text-align: center;
222
+ padding: 10px;
223
+ color: #999;
224
+ font-size: 12px;
225
  }
226
 
227
  .loading.hidden {
228
  display: none;
229
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
  </style>
231
  </head>
232
  <body>
233
  <div class="header">
234
+ <h1>ICONCLASS Model Evaluation</h1>
235
+ <div class="subtitle">
236
+ Comparing predictions from <a href="https://huggingface.co/davanstrien/iconclass-vlm" target="_blank">davanstrien/iconclass-vlm</a> against ground truth labels
237
+ </div>
238
+ <div class="description">
239
+ A vision-language model fine-tuned on Qwen2.5-VL-3B for classifying art and cultural heritage images using ICONCLASS notation — a hierarchical classification system for art and iconography.
240
+ </div>
241
  <div class="stats">
242
+ Showing <span id="loadedCount">0</span> of <span id="totalCount">-</span> test images
 
243
  </div>
244
  </div>
245
 
246
  <div id="gallery" class="gallery"></div>
247
 
248
+ <div class="loading hidden" id="loading">Loading...</div>
249
 
250
  <div class="controls">
251
+ <button id="loadMore" class="load-more">Load more images</button>
252
  </div>
253
 
254
  <script>
 
300
  loadMoreBtn.disabled = true;
301
  loadMoreBtn.textContent = "All Images Loaded";
302
  } else {
303
+ loadMoreBtn.textContent = `Load more images (${
304
  totalRows - currentOffset
305
  } remaining)`;
306
  }
 
334
  const content = document.createElement("div");
335
  content.className = "card-content";
336
 
337
+ // Show raw prediction (collapsible)
338
  if (row["iconclass-prediction"]) {
339
+ const toggleDiv = document.createElement("div");
340
+ toggleDiv.className = "raw-toggle";
341
+ toggleDiv.textContent = "+ Show raw prediction";
342
+
343
  const rawDiv = document.createElement("div");
344
  rawDiv.className = "raw-prediction";
345
  rawDiv.textContent = row["iconclass-prediction"];
346
+
347
+ toggleDiv.addEventListener("click", () => {
348
+ if (rawDiv.classList.contains("visible")) {
349
+ rawDiv.classList.remove("visible");
350
+ toggleDiv.textContent = "+ Show raw prediction";
351
+ } else {
352
+ rawDiv.classList.add("visible");
353
+ toggleDiv.textContent = "− Hide raw prediction";
354
+ }
355
+ });
356
+
357
+ content.appendChild(toggleDiv);
358
  content.appendChild(rawDiv);
359
  }
360
 
 
432
  comparison.appendChild(gtColumn);
433
  content.appendChild(comparison);
434
 
435
+ // Add match statistics
436
  const validPredictions = predictions.filter(
437
  (_, idx) => !invalidPredictions[idx]
438
  );
 
441
  ? Math.round((matches.length / validPredictions.length) * 100)
442
  : 0;
443
 
444
+ const statsDiv = document.createElement("div");
445
+ statsDiv.className = "match-stats";
446
+ statsDiv.textContent = validPredictions.length > 0
447
+ ? `${matches.length}/${validPredictions.length} matches`
448
+ : `No valid predictions`;
449
+ content.appendChild(statsDiv);
450
 
451
  card.appendChild(content);
452
  gallery.appendChild(card);