CrispStrobe commited on
Commit
225d830
·
1 Parent(s): 02e6e85

feat: add interactive metadata info icon with direct Hugging Face links

Browse files
Files changed (3) hide show
  1. scripts/providers/infomaniak.js +6 -5
  2. src/App.css +78 -0
  3. src/App.tsx +23 -1
scripts/providers/infomaniak.js CHANGED
@@ -24,9 +24,9 @@ const { getText } = require('../fetch-utils');
24
 
25
  const URL = 'https://www.infomaniak.com/en/hosting/ai-services/prices';
26
 
27
- const parseChf = (text) => {
28
  if (!text) return null;
29
- if (text.trim().toLowerCase() === 'free') return 0;
30
  const m = text.trim().match(/([\d]+\.[\d]*|[\d]+)/);
31
  return m ? parseFloat(m[1]) : null;
32
  };
@@ -50,6 +50,7 @@ async function fetchInfomaniak() {
50
  'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
51
  Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
52
  'Accept-Language': 'en-US,en;q=0.9',
 
53
  },
54
  });
55
  const $ = cheerio.load(html);
@@ -83,12 +84,12 @@ async function fetchInfomaniak() {
83
  if (currText && /^[A-Z]{3}$/.test(currText)) currency = currText;
84
 
85
  const valSpan = $(priceRow).find('[class*="IkTypography-module--h3"]').first();
86
- const val = parseChf(valSpan.text());
87
  if (val === null) return;
88
 
89
- if (label.includes('incoming') || label.includes('input')) {
90
  inputPrice = val;
91
- } else if (label.includes('outgoing') || label.includes('output')) {
92
  outputPrice = val;
93
  } else if (label.includes('image') || label.includes('per image')) {
94
  inputPrice = val; // image models: price per image stored as input
 
24
 
25
  const URL = 'https://www.infomaniak.com/en/hosting/ai-services/prices';
26
 
27
+ const parsePrice = (text) => {
28
  if (!text) return null;
29
+ if (text.trim().toLowerCase() === 'free' || text.trim().toLowerCase() === 'gratuit') return 0;
30
  const m = text.trim().match(/([\d]+\.[\d]*|[\d]+)/);
31
  return m ? parseFloat(m[1]) : null;
32
  };
 
50
  'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
51
  Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
52
  'Accept-Language': 'en-US,en;q=0.9',
53
+ 'Cookie': 'STATIC_CURRENCY=EUR; locale=en_US',
54
  },
55
  });
56
  const $ = cheerio.load(html);
 
84
  if (currText && /^[A-Z]{3}$/.test(currText)) currency = currText;
85
 
86
  const valSpan = $(priceRow).find('[class*="IkTypography-module--h3"]').first();
87
+ const val = parsePrice(valSpan.text());
88
  if (val === null) return;
89
 
90
+ if (label.includes('entrant') || label.includes('incoming') || label.includes('input')) {
91
  inputPrice = val;
92
+ } else if (label.includes('sortant') || label.includes('outgoing') || label.includes('output')) {
93
  outputPrice = val;
94
  } else if (label.includes('image') || label.includes('per image')) {
95
  inputPrice = val; // image models: price per image stored as input
src/App.css CHANGED
@@ -151,6 +151,84 @@ tr.group-divider {
151
  color: #1e293b;
152
  }
153
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  .size-cell {
155
  font-family: 'JetBrains Mono', monospace;
156
  font-size: 0.85rem;
 
151
  color: #1e293b;
152
  }
153
 
154
+ .model-name-wrapper {
155
+ display: flex;
156
+ align-items: center;
157
+ gap: 0.5rem;
158
+ }
159
+
160
+ .model-info-container {
161
+ position: relative;
162
+ display: inline-flex;
163
+ cursor: help;
164
+ }
165
+
166
+ .info-icon {
167
+ font-size: 0.85rem;
168
+ color: #94a3b8;
169
+ opacity: 0.6;
170
+ transition: opacity 0.2s;
171
+ }
172
+
173
+ .model-info-container:hover .info-icon {
174
+ opacity: 1;
175
+ color: var(--primary-color);
176
+ }
177
+
178
+ .model-tooltip {
179
+ visibility: hidden;
180
+ position: absolute;
181
+ bottom: 100%;
182
+ left: 50%;
183
+ transform: translateX(-50%) translateY(-8px);
184
+ background-color: #1e293b;
185
+ color: #f8fafc;
186
+ padding: 0.75rem;
187
+ border-radius: 0.5rem;
188
+ font-family: 'Inter', sans-serif;
189
+ font-size: 0.75rem;
190
+ font-weight: 400;
191
+ white-space: nowrap;
192
+ z-index: 100;
193
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
194
+ pointer-events: none;
195
+ }
196
+
197
+ .model-info-container:hover .model-tooltip {
198
+ visibility: visible;
199
+ pointer-events: auto;
200
+ }
201
+
202
+ .model-tooltip::after {
203
+ content: "";
204
+ position: absolute;
205
+ top: 100%;
206
+ left: 50%;
207
+ margin-left: -5px;
208
+ border-width: 5px;
209
+ border-style: solid;
210
+ border-color: #1e293b transparent transparent transparent;
211
+ }
212
+
213
+ .tooltip-row {
214
+ margin-bottom: 0.25rem;
215
+ }
216
+ .tooltip-row:last-child {
217
+ margin-bottom: 0;
218
+ }
219
+ .tooltip-row strong {
220
+ color: #94a3b8;
221
+ margin-right: 0.25rem;
222
+ }
223
+
224
+ .hf-link {
225
+ color: #60a5fa;
226
+ text-decoration: none;
227
+ }
228
+ .hf-link:hover {
229
+ text-decoration: underline;
230
+ }
231
+
232
  .size-cell {
233
  font-family: 'JetBrains Mono', monospace;
234
  font-size: 0.85rem;
src/App.tsx CHANGED
@@ -500,7 +500,29 @@ function App() {
500
  {model.complianceStatus}
501
  </span>
502
  </td>
503
- <td className="model-name">{model.display_name ?? model.name}</td>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
504
  <td className="caps-cell">
505
  {model.type === 'embedding' && (
506
  <span className="cap-badge cap-embedding" title="embedding">{CAP_ICON.embedding}</span>
 
500
  {model.complianceStatus}
501
  </span>
502
  </td>
503
+ <td className="model-name">
504
+ <div className="model-name-wrapper">
505
+ {model.display_name ?? model.name}
506
+ <div className="model-info-container">
507
+ <span className="info-icon">ⓘ</span>
508
+ <div className="model-tooltip">
509
+ <div className="tooltip-row"><strong>Type:</strong> {model.type}</div>
510
+ {model.size_b && <div className="tooltip-row"><strong>Size:</strong> {model.size_b}B</div>}
511
+ {model.hf_id && (
512
+ <div className="tooltip-row">
513
+ <strong>HF:</strong>
514
+ <a href={`https://huggingface.co/${model.hf_id}`} target="_blank" rel="noopener noreferrer" className="hf-link">
515
+ {model.hf_id} ↗
516
+ </a>
517
+ </div>
518
+ )}
519
+ {model.capabilities && model.capabilities.length > 0 && (
520
+ <div className="tooltip-row"><strong>Caps:</strong> {model.capabilities.join(', ')}</div>
521
+ )}
522
+ </div>
523
+ </div>
524
+ </div>
525
+ </td>
526
  <td className="caps-cell">
527
  {model.type === 'embedding' && (
528
  <span className="cap-badge cap-embedding" title="embedding">{CAP_ICON.embedding}</span>