free-coding-models 0.4.3 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -1
- package/bin/free-coding-models.js +19 -9
- package/changelog/v0.5.0.md +15 -0
- package/changelog/v0.5.1.md +24 -0
- package/package.json +7 -2
- package/src/{analysis.js → core/analysis.js} +5 -5
- package/src/{constants.js → core/constants.js} +1 -1
- package/src/{endpoint-installer.js → core/endpoint-installer.js} +1 -1
- package/src/{installed-models-manager.js → core/installed-models-manager.js} +1 -1
- package/src/{kilo.js → core/kilo.js} +1 -2
- package/src/{openclaw.js → core/openclaw.js} +1 -1
- package/src/{opencode.js → core/opencode.js} +2 -1
- package/src/{ping-loop.js → core/ping-loop.js} +1 -1
- package/src/{router-daemon.js → core/router-daemon.js} +169 -4
- package/src/{router-dashboard.js → core/router-dashboard.js} +2 -2
- package/src/{setup.js → core/setup.js} +1 -1
- package/src/{sync-set.js → core/sync-set.js} +1 -1
- package/src/{telemetry.js → core/telemetry.js} +1 -1
- package/src/{tool-launchers.js → core/tool-launchers.js} +2 -2
- package/src/{updater.js → core/updater.js} +1 -1
- package/src/{utils.js → core/utils.js} +2 -0
- package/src/{app.js → tui/app.js} +38 -38
- package/src/{cli-help.js → tui/cli-help.js} +3 -1
- package/src/{command-palette.js → tui/command-palette.js} +2 -2
- package/src/{key-handler.js → tui/key-handler.js} +11 -11
- package/src/{overlays.js → tui/overlays.js} +2 -2
- package/src/{render-helpers.js → tui/render-helpers.js} +2 -2
- package/src/{render-table.js → tui/render-table.js} +9 -9
- package/src/{tui-filters.js → tui/tui-filters.js} +3 -3
- package/src/{tui-state.js → tui/tui-state.js} +1 -1
- package/web/README.md +46 -0
- package/web/dist/assets/index-ByGf4Kq-.js +14 -0
- package/web/dist/assets/index-Ds7wmHBv.css +1 -0
- package/web/dist/index.html +3 -6
- package/web/index.html +1 -4
- package/web/package.json +11 -0
- package/web/server.js +609 -214
- package/web/src/App.jsx +54 -12
- package/web/src/components/analytics/AnalyticsView.jsx +10 -4
- package/web/src/components/atoms/AILatencyCell.jsx +38 -0
- package/web/src/components/atoms/AILatencyCell.module.css +43 -0
- package/web/src/components/atoms/HealthCell.jsx +53 -0
- package/web/src/components/atoms/HealthCell.module.css +15 -0
- package/web/src/components/atoms/LastPingCell.jsx +35 -0
- package/web/src/components/atoms/LastPingCell.module.css +35 -0
- package/web/src/components/atoms/MoodCell.jsx +25 -0
- package/web/src/components/atoms/MoodCell.module.css +6 -0
- package/web/src/components/atoms/RankCell.jsx +9 -0
- package/web/src/components/atoms/RankCell.module.css +9 -0
- package/web/src/components/atoms/TPSCell.jsx +36 -0
- package/web/src/components/atoms/TPSCell.module.css +38 -0
- package/web/src/components/atoms/VerdictBadge.jsx +30 -7
- package/web/src/components/atoms/VerdictBadge.module.css +24 -15
- package/web/src/components/dashboard/ExportModal.jsx +9 -4
- package/web/src/components/dashboard/FilterBar.jsx +112 -10
- package/web/src/components/dashboard/FilterBar.module.css +86 -1
- package/web/src/components/dashboard/ModelTable.jsx +293 -52
- package/web/src/components/dashboard/ModelTable.module.css +131 -33
- package/web/src/components/dashboard/StatsBar.jsx +7 -5
- package/web/src/components/layout/Footer.jsx +1 -1
- package/web/src/components/layout/Header.jsx +43 -9
- package/web/src/components/layout/Header.module.css +38 -4
- package/web/src/components/layout/Sidebar.jsx +19 -11
- package/web/src/components/layout/Sidebar.module.css +15 -5
- package/web/src/components/settings/SettingsView.jsx +24 -6
- package/web/src/components/settings/SettingsView.module.css +0 -1
- package/web/src/global.css +70 -73
- package/web/src/hooks/useFilter.js +117 -25
- package/web/src/hooks/useSSE.js +33 -9
- package/web/src/hooks/useSocket.js +200 -0
- package/web/vite.config.js +41 -0
- package/src/graphify-out/cache/089db1c1def873cf6d112f1590da4490e61e691aff0db41e006aa2fb15ba0656.json +0 -1
- package/src/graphify-out/cache/0b510b53cf1a1393fb52b1fc3bbbf88b63938e961ec5b82119a2e9715fee8bd7.json +0 -1
- package/src/graphify-out/cache/0ec9a95a326bde58e0316889018b278062d06d494d0f31ba177c9de71e5fed2d.json +0 -1
- package/src/graphify-out/cache/1548663a24a68dce740ebab1bd1d3091048c9604e9d067a1650a42a6d82541d4.json +0 -1
- package/src/graphify-out/cache/1783af63cb6d0dfb4d469009f71ac83a74ba0b33d48186ff2c6e63f9429e900a.json +0 -1
- package/src/graphify-out/cache/1e109f5eb5dc4fd285871c3613e32b6b14a8c225f4080ee34b51c7e1a1764571.json +0 -1
- package/src/graphify-out/cache/1eb24dbeb69b46c8bc1caf925df2f2a964af0f33aea143adf8ddf88e017db6ca.json +0 -1
- package/src/graphify-out/cache/21e1bcfed11685e8347243f9d8516072dda183266a4bfe22c52fb31753a446c8.json +0 -1
- package/src/graphify-out/cache/2327473478b9c4b1940bf7ef66c9ee960b3cba8d5302e56b625df8274246e0b4.json +0 -1
- package/src/graphify-out/cache/25955b81fd25454c8fa90fb71a47db8d1215cf621beb8ff3cbd580aaf011b4f3.json +0 -1
- package/src/graphify-out/cache/2739677f19c702f88f3de0a0bac475066adbda98709907ad3de967aef689f86d.json +0 -1
- package/src/graphify-out/cache/2bba03422f6b3ee7f5b5d29cc90314a064d259e5822a176657bda3e04505cf00.json +0 -1
- package/src/graphify-out/cache/2ddf1d2c6d10147b0402446bc71a7988187b79b6210dd7e7250be8c555b9ff35.json +0 -1
- package/src/graphify-out/cache/2ee07457a5767c95a57f8e9eb95b28f800044f35666e0715e9d88ad1103a092e.json +0 -1
- package/src/graphify-out/cache/2fe9f75dc2951c417f2c8dd22749092cf550dc67599f1c8d1866900dc6e9154e.json +0 -1
- package/src/graphify-out/cache/41c4b7c27e7fc3e2948d3a4bf95a72de2ed9a6f0463994babdce8ed2cc84598c.json +0 -1
- package/src/graphify-out/cache/5028defd54b7fbd3c7e444973e493de036e097e9b1d2a7cae7f19b88d68aacde.json +0 -1
- package/src/graphify-out/cache/5b133aba3fb16410c5b1fdbd1730039fc7fa1ac93abd99d7be08f60da70fc8d4.json +0 -1
- package/src/graphify-out/cache/74252e5b0978d85ab3421a3de1a9384aa282ffd2be2cfe7db2530139089f4275.json +0 -1
- package/src/graphify-out/cache/7695ebeea056095edd14332963cc43354ef3a097caf46f1e28d0f01369642901.json +0 -1
- package/src/graphify-out/cache/777aa7085c395a935c6556bbde182cd871edb61f3a685ed8068ec0c8f6fb0075.json +0 -1
- package/src/graphify-out/cache/82a723881980e82273c113def8315533d7da28827e300413d9ad30f27b7407df.json +0 -1
- package/src/graphify-out/cache/86b87c9603e6cd188f42c7eed3b86c291d48a781c223a707e74f3e7ed0c02a21.json +0 -1
- package/src/graphify-out/cache/890fead9a78cadaed560a2d2453916121fa605c3e43a334910ac4bc951a9ef6d.json +0 -1
- package/src/graphify-out/cache/89d3ea66f52783caa775ef9a30923d7d6225e1d8ae9e962f4741b8c7785dab1e.json +0 -1
- package/src/graphify-out/cache/8cc82cd9edce41f0e1c092f14a94fd52bf847addf3237b616dc5a9e505bd05bd.json +0 -1
- package/src/graphify-out/cache/93ba2e25e3ff7ad525f397902345fbd375df7315de7b402e20cc803c14eccde8.json +0 -1
- package/src/graphify-out/cache/99beed29580b9c7bfecfee794cb3d8e535fcf0eb3b92113108f88bdd0a8e79b3.json +0 -1
- package/src/graphify-out/cache/aeeb931fa477c65ce2e51d8149957350fa54225c613222bbbe8448998d1afd3d.json +0 -1
- package/src/graphify-out/cache/baf91bef5b5ecb2a476433b6cc0c48c563c54ee2d07fc3c192e543685e3e7222.json +0 -1
- package/src/graphify-out/cache/bd98b94ac4e9b92b6336d47b26e0366b51a4eaf0711d722f05f98dfae23ab42b.json +0 -1
- package/src/graphify-out/cache/bfcb51e9328e9cbfbee4f6fee0f56635d7b03488addc9f6c4e4b190b70a73362.json +0 -1
- package/src/graphify-out/cache/c0d3dabeb093aa758c49eadf41b87ecc96a16c1449c2670aaf48cbfc891d8da6.json +0 -1
- package/src/graphify-out/cache/c20d6630236f473c1406068c3ae205853e649b216495c93dfec055dd222c55cf.json +0 -1
- package/src/graphify-out/cache/c22b9122816bebce0a2f79af41a986559d01e00163dbcd579c5755621b4cb483.json +0 -1
- package/src/graphify-out/cache/ca556ec14453ddb8f9e0c5a832dac90d77111b9bad5f8c2d80d272e2e7a06371.json +0 -1
- package/src/graphify-out/cache/d6dbc9135dfa35a756b3b09b06700e4bc229fdccba11bb963f2ba44028e0bbae.json +0 -1
- package/src/graphify-out/cache/e1cf71276f1779d0fa075f79bd7c8a9fd0b8eef6932ac043137451b7c7fa7cbe.json +0 -1
- package/src/graphify-out/cache/e4b3be14494467df2d2ed389bc4f18f099021cb5bc355b901fa88387b2d8b8a2.json +0 -1
- package/src/graphify-out/cache/eaea0dded097f6f9553b654220046c6ec0c9be592a5973d906564ee60af34e0d.json +0 -1
- package/src/graphify-out/cache/ef07d0cd2675d1f79d2a2fdbf3bc3319687638751e9ce89b0d0d97ed1cd9f7e1.json +0 -1
- package/src/graphify-out/cache/f81272d6eb8aaff9e96d5a1d9f06777db70ac3652a646b951ded51f79871d733.json +0 -1
- package/src/graphify-out/cache/f9619dd92186f75a6dbda937e0c606647153918524cdb5763f956e6ec2a9e386.json +0 -1
- package/src/graphify-out/cache/fd88b1b2ff4bfcae08559d9c2aaeeb9a3f1e2f5cd8928762c311196956c170a5.json +0 -1
- package/web/dist/assets/index-CGN-0_A0.css +0 -1
- package/web/dist/assets/index-Czwis3ab.js +0 -11
- /package/src/{benchmark.js → core/benchmark.js} +0 -0
- /package/src/{cache.js → core/cache.js} +0 -0
- /package/src/{changelog-loader.js → core/changelog-loader.js} +0 -0
- /package/src/{config.js → core/config.js} +0 -0
- /package/src/{favorites.js → core/favorites.js} +0 -0
- /package/src/{kilo-config.js → core/kilo-config.js} +0 -0
- /package/src/{legacy-proxy-cleanup.js → core/legacy-proxy-cleanup.js} +0 -0
- /package/src/{model-merger.js → core/model-merger.js} +0 -0
- /package/src/{opencode-config.js → core/opencode-config.js} +0 -0
- /package/src/{ping.js → core/ping.js} +0 -0
- /package/src/{product-flags.js → core/product-flags.js} +0 -0
- /package/src/{provider-metadata.js → core/provider-metadata.js} +0 -0
- /package/src/{provider-quota-fetchers.js → core/provider-quota-fetchers.js} +0 -0
- /package/src/{quota-capabilities.js → core/quota-capabilities.js} +0 -0
- /package/src/{security.js → core/security.js} +0 -0
- /package/src/{shell-env.js → core/shell-env.js} +0 -0
- /package/src/{testfcm.js → core/testfcm.js} +0 -0
- /package/src/{token-usage-reader.js → core/token-usage-reader.js} +0 -0
- /package/src/{tool-bootstrap.js → core/tool-bootstrap.js} +0 -0
- /package/src/{tool-metadata.js → core/tool-metadata.js} +0 -0
- /package/src/{usage-reader.js → core/usage-reader.js} +0 -0
- /package/src/{mouse.js → tui/mouse.js} +0 -0
- /package/src/{theme.js → tui/theme.js} +0 -0
- /package/src/{tier-colors.js → tui/tier-colors.js} +0 -0
- /package/src/{ui-config.js → tui/ui-config.js} +0 -0
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* `onClose` callback, and `onToast` for feedback notifications.
|
|
6
6
|
* @functions ExportModal → renders the modal with three export option buttons
|
|
7
7
|
*/
|
|
8
|
+
import { IconUpload, IconBraces, IconFileSpreadsheet, IconClipboard } from '@tabler/icons-react'
|
|
8
9
|
import { downloadFile } from '../../utils/download.js'
|
|
9
10
|
import styles from './ExportModal.module.css'
|
|
10
11
|
|
|
@@ -45,27 +46,30 @@ export default function ExportModal({ models, onClose, onToast }) {
|
|
|
45
46
|
<div className={styles.overlay} onClick={(e) => e.target === e.currentTarget && onClose()}>
|
|
46
47
|
<div className={styles.modal}>
|
|
47
48
|
<div className={styles.modalHeader}>
|
|
48
|
-
<h2 className={styles.modalTitle}
|
|
49
|
+
<h2 className={styles.modalTitle}>
|
|
50
|
+
<IconUpload size={20} stroke={1.5} style={{ marginRight: 8, verticalAlign: 'middle' }} />
|
|
51
|
+
Export Data
|
|
52
|
+
</h2>
|
|
49
53
|
<button className={styles.modalClose} onClick={onClose}>×</button>
|
|
50
54
|
</div>
|
|
51
55
|
<div className={styles.modalBody}>
|
|
52
56
|
<div className={styles.options}>
|
|
53
57
|
<button className={styles.option} onClick={handleJson}>
|
|
54
|
-
<span className={styles.optionIcon}
|
|
58
|
+
<span className={styles.optionIcon}><IconBraces size={20} stroke={1.5} /></span>
|
|
55
59
|
<div>
|
|
56
60
|
<span className={styles.optionLabel}>Export as JSON</span>
|
|
57
61
|
<span className={styles.optionDesc}>Full model data with all metrics</span>
|
|
58
62
|
</div>
|
|
59
63
|
</button>
|
|
60
64
|
<button className={styles.option} onClick={handleCsv}>
|
|
61
|
-
<span className={styles.optionIcon}
|
|
65
|
+
<span className={styles.optionIcon}><IconFileSpreadsheet size={20} stroke={1.5} /></span>
|
|
62
66
|
<div>
|
|
63
67
|
<span className={styles.optionLabel}>Export as CSV</span>
|
|
64
68
|
<span className={styles.optionDesc}>Spreadsheet-compatible format</span>
|
|
65
69
|
</div>
|
|
66
70
|
</button>
|
|
67
71
|
<button className={styles.option} onClick={handleClipboard}>
|
|
68
|
-
<span className={styles.optionIcon}
|
|
72
|
+
<span className={styles.optionIcon}><IconClipboard size={20} stroke={1.5} /></span>
|
|
69
73
|
<div>
|
|
70
74
|
<span className={styles.optionLabel}>Copy to Clipboard</span>
|
|
71
75
|
<span className={styles.optionDesc}>Copy model summary as text</span>
|
|
@@ -77,3 +81,4 @@ export default function ExportModal({ models, onClose, onToast }) {
|
|
|
77
81
|
</div>
|
|
78
82
|
)
|
|
79
83
|
}
|
|
84
|
+
|
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file web/src/components/dashboard/FilterBar.jsx
|
|
3
|
-
* @description Filter controls for tier, status, provider,
|
|
3
|
+
* @description Filter controls for tier, status, provider, ping interval, and live indicator.
|
|
4
|
+
* 📖 Shows "Next ping in Xs" countdown matching TUI behavior. Ping mode selector (Speed/Normal/Slow/Forced).
|
|
4
5
|
*/
|
|
6
|
+
import { useState, useEffect } from 'react'
|
|
5
7
|
import styles from './FilterBar.module.css'
|
|
6
8
|
|
|
7
|
-
const TIERS = [
|
|
9
|
+
const TIERS = [
|
|
10
|
+
{ key: 'all', label: 'All' },
|
|
11
|
+
{ key: 'S+', label: 'S+' },
|
|
12
|
+
{ key: 'S', label: 'S' },
|
|
13
|
+
{ key: 'A+', label: 'A+' },
|
|
14
|
+
{ key: 'A', label: 'A' },
|
|
15
|
+
{ key: 'A-', label: 'A-' },
|
|
16
|
+
{ key: 'B+', label: 'B+' },
|
|
17
|
+
{ key: 'B', label: 'B' },
|
|
18
|
+
{ key: 'C', label: 'C' },
|
|
19
|
+
]
|
|
8
20
|
const STATUSES = [
|
|
9
21
|
{ key: 'all', label: 'All' },
|
|
10
22
|
{ key: 'up', label: 'Online' },
|
|
@@ -12,24 +24,82 @@ const STATUSES = [
|
|
|
12
24
|
{ key: 'pending', label: 'Pending' },
|
|
13
25
|
]
|
|
14
26
|
|
|
27
|
+
const PING_MODES = [
|
|
28
|
+
{ key: 'speed', label: '⚡ Speed', interval: '2s', color: '#00ff88' },
|
|
29
|
+
{ key: 'normal', label: '● Normal', interval: '10s', color: '#ffaa00' },
|
|
30
|
+
{ key: 'slow', label: '🐢 Slow', interval: '30s', color: '#ff6644' },
|
|
31
|
+
{ key: 'forced', label: '🔥 Forced', interval: '4s', color: '#ff4466' },
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
function formatCountdown(ms) {
|
|
35
|
+
if (ms == null) return null
|
|
36
|
+
const s = Math.max(0, Math.ceil(ms / 1000))
|
|
37
|
+
if (s === 1) return '1s'
|
|
38
|
+
if (s < 60) return `${s}s`
|
|
39
|
+
const m = Math.floor(s / 60)
|
|
40
|
+
const rem = s % 60
|
|
41
|
+
return `${m}m${rem > 0 ? rem + 's' : ''}`
|
|
42
|
+
}
|
|
43
|
+
|
|
15
44
|
export default function FilterBar({
|
|
16
45
|
filterTier, setFilterTier,
|
|
17
46
|
filterStatus, setFilterStatus,
|
|
18
47
|
filterProvider, setFilterProvider,
|
|
19
48
|
providers,
|
|
49
|
+
pingMode, setPingMode,
|
|
50
|
+
nextPingAt,
|
|
51
|
+
isPinging,
|
|
52
|
+
globalBenchmarkRunning,
|
|
53
|
+
globalBenchmarkTotal,
|
|
54
|
+
globalBenchmarkCompleted,
|
|
20
55
|
}) {
|
|
56
|
+
const [countdown, setCountdown] = useState(null)
|
|
57
|
+
|
|
58
|
+
// Update countdown every second when nextPingAt is set
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (nextPingAt == null) return
|
|
61
|
+
const tick = () => {
|
|
62
|
+
const rem = nextPingAt - Date.now()
|
|
63
|
+
setCountdown(rem > 0 ? rem : 0)
|
|
64
|
+
}
|
|
65
|
+
tick()
|
|
66
|
+
const id = setInterval(tick, 1000)
|
|
67
|
+
return () => clearInterval(id)
|
|
68
|
+
}, [nextPingAt])
|
|
69
|
+
|
|
70
|
+
const countdownDisplay = countdown !== null ? formatCountdown(countdown) : null
|
|
71
|
+
|
|
72
|
+
// Global benchmark progress percentage
|
|
73
|
+
const benchmarkPct = globalBenchmarkRunning && globalBenchmarkTotal > 0
|
|
74
|
+
? Math.round((globalBenchmarkCompleted / globalBenchmarkTotal) * 100)
|
|
75
|
+
: 0
|
|
76
|
+
|
|
21
77
|
return (
|
|
22
78
|
<section className={styles.filters}>
|
|
79
|
+
{/* ── Global benchmark progress bar (Ctrl+U) ── */}
|
|
80
|
+
{globalBenchmarkRunning && (
|
|
81
|
+
<div className={styles.benchmarkBar}>
|
|
82
|
+
<div className={styles.benchmarkLabel}>
|
|
83
|
+
<span className={styles.benchmarkSpinner} />
|
|
84
|
+
<span>AI Speed Test</span>
|
|
85
|
+
<span className={styles.benchmarkCount}>{globalBenchmarkCompleted}/{globalBenchmarkTotal}</span>
|
|
86
|
+
</div>
|
|
87
|
+
<div className={styles.benchmarkTrack}>
|
|
88
|
+
<div className={styles.benchmarkFill} style={{ width: `${benchmarkPct}%` }} />
|
|
89
|
+
</div>
|
|
90
|
+
<span className={styles.benchmarkPct}>{benchmarkPct}%</span>
|
|
91
|
+
</div>
|
|
92
|
+
)}
|
|
23
93
|
<div className={styles.group}>
|
|
24
94
|
<label className={styles.filterLabel}>Tier</label>
|
|
25
95
|
<div className={styles.tierRow}>
|
|
26
96
|
{TIERS.map(t => (
|
|
27
97
|
<button
|
|
28
|
-
key={t}
|
|
29
|
-
className={`${styles.tierBtn} ${filterTier === t ? styles.active : ''}`}
|
|
30
|
-
onClick={() => setFilterTier(t)}
|
|
98
|
+
key={t.key}
|
|
99
|
+
className={`${styles.tierBtn} ${filterTier === t.key ? styles.active : ''}`}
|
|
100
|
+
onClick={() => setFilterTier(t.key)}
|
|
31
101
|
>
|
|
32
|
-
{t}
|
|
102
|
+
{t.label}
|
|
33
103
|
</button>
|
|
34
104
|
))}
|
|
35
105
|
</div>
|
|
@@ -62,12 +132,44 @@ export default function FilterBar({
|
|
|
62
132
|
</select>
|
|
63
133
|
</div>
|
|
64
134
|
<div className={styles.spacer} />
|
|
135
|
+
|
|
136
|
+
{/* ── Ping interval selector ── */}
|
|
65
137
|
<div className={styles.group}>
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
|
|
138
|
+
<label className={styles.filterLabel}>Ping</label>
|
|
139
|
+
<div className={styles.pingRow}>
|
|
140
|
+
{PING_MODES.map(m => (
|
|
141
|
+
<button
|
|
142
|
+
key={m.key}
|
|
143
|
+
className={`${styles.pingBtn} ${pingMode === m.key ? styles.pingBtnActive : ''}`}
|
|
144
|
+
style={pingMode === m.key ? { '--ping-active-color': m.color } : {}}
|
|
145
|
+
onClick={() => setPingMode(m.key)}
|
|
146
|
+
title={`${m.interval} interval`}
|
|
147
|
+
>
|
|
148
|
+
{m.label}
|
|
149
|
+
</button>
|
|
150
|
+
))}
|
|
69
151
|
</div>
|
|
70
152
|
</div>
|
|
153
|
+
|
|
154
|
+
{/* ── Next ping countdown + LIVE ── */}
|
|
155
|
+
<div className={styles.group}>
|
|
156
|
+
{isPinging ? (
|
|
157
|
+
<div className={styles.nextPing} title="Pinging now…">
|
|
158
|
+
<span className={styles.pingingDot} />
|
|
159
|
+
<span className={styles.pingingText}>Pinging…</span>
|
|
160
|
+
</div>
|
|
161
|
+
) : countdownDisplay ? (
|
|
162
|
+
<div className={styles.nextPing} title="Next ping countdown">
|
|
163
|
+
<span className={styles.nextPingLabel}>Next</span>
|
|
164
|
+
<span className={styles.nextPingTime}>{countdownDisplay}</span>
|
|
165
|
+
</div>
|
|
166
|
+
) : (
|
|
167
|
+
<div className={styles.live}>
|
|
168
|
+
<span className={styles.liveDot} />
|
|
169
|
+
<span>LIVE</span>
|
|
170
|
+
</div>
|
|
171
|
+
)}
|
|
172
|
+
</div>
|
|
71
173
|
</section>
|
|
72
174
|
)
|
|
73
|
-
}
|
|
175
|
+
}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
}
|
|
18
18
|
.tierBtn:hover { background: var(--color-bg-hover); color: var(--color-text); }
|
|
19
19
|
.active {
|
|
20
|
-
background: var(--color-accent) !important; color:
|
|
20
|
+
background: var(--color-accent) !important; color: var(--color-bg) !important;
|
|
21
21
|
border-color: var(--color-accent) !important; font-weight: 700;
|
|
22
22
|
}
|
|
23
23
|
.providerSelect {
|
|
@@ -37,7 +37,92 @@
|
|
|
37
37
|
width: 8px; height: 8px; border-radius: 50%;
|
|
38
38
|
background: var(--color-accent); animation: pulseDot 2s ease-in-out infinite;
|
|
39
39
|
}
|
|
40
|
+
|
|
41
|
+
/* ── Next ping countdown ── */
|
|
42
|
+
.nextPing {
|
|
43
|
+
display: flex; align-items: center; gap: 6px;
|
|
44
|
+
font-size: 12px; font-family: var(--font-mono);
|
|
45
|
+
}
|
|
46
|
+
.nextPingLabel {
|
|
47
|
+
font-size: 11px; font-weight: 500; color: var(--color-text-muted);
|
|
48
|
+
text-transform: uppercase; letter-spacing: 0.5px;
|
|
49
|
+
}
|
|
50
|
+
.nextPingTime {
|
|
51
|
+
font-size: 14px; font-weight: 700; color: var(--color-text);
|
|
52
|
+
min-width: 32px;
|
|
53
|
+
}
|
|
54
|
+
.pingingDot {
|
|
55
|
+
width: 8px; height: 8px; border-radius: 50%;
|
|
56
|
+
background: #ff4466; animation: pulseDot 0.6s ease-in-out infinite;
|
|
57
|
+
}
|
|
58
|
+
.pingingText {
|
|
59
|
+
font-size: 12px; font-weight: 600; color: #ff4466;
|
|
60
|
+
font-family: var(--font-mono);
|
|
61
|
+
}
|
|
62
|
+
|
|
40
63
|
@keyframes pulseDot {
|
|
41
64
|
0%, 100% { box-shadow: 0 0 0 0 var(--color-accent-glow); }
|
|
42
65
|
50% { box-shadow: 0 0 0 6px transparent; }
|
|
43
66
|
}
|
|
67
|
+
|
|
68
|
+
/* ── Ping interval selector ── */
|
|
69
|
+
.pingRow { display: flex; gap: 4px; }
|
|
70
|
+
.pingBtn {
|
|
71
|
+
font-size: 12px; font-weight: 600; font-family: var(--font-mono);
|
|
72
|
+
padding: 4px 10px; border-radius: 6px;
|
|
73
|
+
border: 1px solid var(--color-border); background: var(--color-surface);
|
|
74
|
+
color: var(--color-text-muted); cursor: pointer; transition: all 150ms;
|
|
75
|
+
white-space: nowrap;
|
|
76
|
+
}
|
|
77
|
+
.pingBtn:hover { background: var(--color-bg-hover); color: var(--color-text); border-color: var(--color-text-muted); }
|
|
78
|
+
.pingBtnActive {
|
|
79
|
+
background: var(--color-bg-hover) !important;
|
|
80
|
+
color: var(--ping-active-color, var(--color-accent)) !important;
|
|
81
|
+
border-color: var(--ping-active-color, var(--color-accent)) !important;
|
|
82
|
+
font-weight: 700;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* ── Global benchmark progress bar (Ctrl+U) ── */
|
|
86
|
+
.benchmarkBar {
|
|
87
|
+
display: flex; align-items: center; gap: 12px;
|
|
88
|
+
background: rgba(180, 0, 255, 0.12);
|
|
89
|
+
border: 1px solid rgba(180, 0, 255, 0.4);
|
|
90
|
+
border-radius: 8px;
|
|
91
|
+
padding: 8px 14px;
|
|
92
|
+
flex-shrink: 0;
|
|
93
|
+
}
|
|
94
|
+
.benchmarkLabel {
|
|
95
|
+
display: flex; align-items: center; gap: 6px;
|
|
96
|
+
font-size: 12px; font-weight: 700;
|
|
97
|
+
color: #b400ff; font-family: var(--font-mono);
|
|
98
|
+
white-space: nowrap;
|
|
99
|
+
}
|
|
100
|
+
.benchmarkSpinner {
|
|
101
|
+
display: inline-block;
|
|
102
|
+
animation: spinSp 0.8s linear infinite;
|
|
103
|
+
font-size: 14px; line-height: 1;
|
|
104
|
+
}
|
|
105
|
+
.benchmarkCount {
|
|
106
|
+
font-size: 11px; opacity: 0.8;
|
|
107
|
+
}
|
|
108
|
+
.benchmarkTrack {
|
|
109
|
+
width: 120px; height: 6px;
|
|
110
|
+
background: rgba(255,255,255,0.1);
|
|
111
|
+
border-radius: 3px; overflow: hidden;
|
|
112
|
+
}
|
|
113
|
+
.benchmarkFill {
|
|
114
|
+
height: 100%;
|
|
115
|
+
background: linear-gradient(90deg, #b400ff, #ff00ff);
|
|
116
|
+
border-radius: 3px;
|
|
117
|
+
transition: width 0.5s ease;
|
|
118
|
+
}
|
|
119
|
+
.benchmarkPct {
|
|
120
|
+
font-size: 12px; font-weight: 700;
|
|
121
|
+
color: #b400ff; font-family: var(--font-mono);
|
|
122
|
+
min-width: 36px; text-align: right;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
@keyframes spinSp {
|
|
126
|
+
from { transform: rotate(0deg); }
|
|
127
|
+
to { transform: rotate(360deg); }
|
|
128
|
+
}
|