@goodandready/dsh-cost-meter 0.7.8 → 0.8.0
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 +14 -8
- package/docs/README.ru.md +14 -8
- package/docs/design/DESIGN.md +7 -3
- package/docs/plans/findings.md +11 -8
- package/docs/plans/progress.md +24 -16
- package/docs/plans/task_plan.md +23 -22
- package/lib/client.js +500 -309
- package/lib/index.js +530 -515
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -1,15 +1,7 @@
|
|
|
1
|
-
// dsh-cost-meter —
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
// counts come from dsh-token-meter's `tokenUsage` projection and move while a
|
|
6
|
-
// turn streams. The tariff schedule arrives from the host over GET
|
|
7
|
-
// /dsh-cost-meter/state — a base rate card plus UTC discount windows.
|
|
8
|
-
//
|
|
9
|
-
// The client owns only the clock: which window is active now, when it flips,
|
|
10
|
-
// and which variant of the day is the cheap one.
|
|
11
|
-
//
|
|
12
|
-
// Not ESM: the shell loads this file as a lazy CJS factory. No import, no JSX.
|
|
1
|
+
// dsh-cost-meter — client half.
|
|
2
|
+
// Self-registering browser module for DeepSeek Harness.
|
|
3
|
+
// Unified design system aligned with @goodandready/dsh-clinebot.
|
|
4
|
+
|
|
13
5
|
window.__ModuleLoader__.load({
|
|
14
6
|
id: '@goodandready/dsh-cost-meter',
|
|
15
7
|
factory: (require) => {
|
|
@@ -17,123 +9,98 @@ window.__ModuleLoader__.load({
|
|
|
17
9
|
const React = require('react')
|
|
18
10
|
const h = React.createElement
|
|
19
11
|
|
|
12
|
+
const NS = 'dsh-cost-meter'
|
|
20
13
|
const STATE_PATH = '/dsh-cost-meter/state'
|
|
21
14
|
const DAY = 1440
|
|
22
|
-
const NS = 'dsh-cost-meter'
|
|
23
15
|
|
|
24
|
-
|
|
25
|
-
title: 'Cost Meter',
|
|
26
|
-
subtitle: 'Session cost in header, token rates, and per-model breakdown',
|
|
27
|
-
chipUnknown: 'tariff ?',
|
|
28
|
-
chipRate: 'rate',
|
|
29
|
-
offPeak: 'off-peak',
|
|
30
|
-
peak: 'peak',
|
|
31
|
-
flat: 'flat',
|
|
32
|
-
modelsBreakdown: 'Model Breakdown',
|
|
33
|
-
sessionTokens: 'Session Tokens',
|
|
34
|
-
totalCost: 'Total Cost',
|
|
35
|
-
noPriceSpecified: 'rate not specified',
|
|
36
|
-
noUsageYet: 'Provider has not reported usage for this session yet.',
|
|
37
|
-
routeUnknown: 'route unknown',
|
|
38
|
-
catalogUnavailable: 'OpenRouter catalog unavailable: ',
|
|
39
|
-
modelNotFound: 'Model not found in OpenRouter catalog. Set manual prices in settings (prices) or map model via modelMap.',
|
|
40
|
-
'settings.loading': 'Loading settings…',
|
|
41
|
-
'settings.unavailable': 'Settings unavailable (namespace is not registered yet).',
|
|
42
|
-
'settings.save': 'Save',
|
|
43
|
-
'settings.saving': 'Saving…',
|
|
44
|
-
'settings.saved': 'Saved',
|
|
45
|
-
'settings.saveFailed': 'Failed to save settings: ',
|
|
46
|
-
'field.currency': 'Currency symbol',
|
|
47
|
-
'field.usdRate': 'USD exchange rate',
|
|
48
|
-
'field.displayTimeZone': 'Display timezone',
|
|
49
|
-
'hint.currency': 'Currency symbol shown in the widget (e.g. $, ₽, €).',
|
|
50
|
-
'hint.usdRate': 'Exchange rate: units of currency per 1 USD (e.g. 80 for ₽).',
|
|
51
|
-
'hint.displayTimeZone': 'IANA timezone used for tariff peak/off-peak schedule.',
|
|
52
|
-
}
|
|
16
|
+
// ----------------------------------------------------------- localization
|
|
53
17
|
|
|
54
18
|
const ru = {
|
|
55
|
-
title: '
|
|
56
|
-
subtitle: '
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
'
|
|
71
|
-
'
|
|
72
|
-
'
|
|
73
|
-
'
|
|
74
|
-
'settings.saved': 'Сохранено',
|
|
75
|
-
'settings.saveFailed': 'Не удалось сохранить настройки: ',
|
|
19
|
+
title: 'Учёт расходов',
|
|
20
|
+
subtitle: 'Тариф моделей, окна пиковых цен и стоимость сессии',
|
|
21
|
+
'header.title': 'Учёт расходов (Cost Meter)',
|
|
22
|
+
'header.sub': 'Мониторинг расходов токенов, автоматическое сопоставление тарифов и скидочных окон',
|
|
23
|
+
'badge.catalog_ok': 'Каталог: {count} моделей',
|
|
24
|
+
'badge.catalog_empty': 'Каталог: загрузка…',
|
|
25
|
+
'badge.catalog_off': 'Каталог: выключен',
|
|
26
|
+
'badge.catalog_err': 'Каталог: ошибка',
|
|
27
|
+
'badge.deepseek': 'Тариф DeepSeek: активен',
|
|
28
|
+
'section.general': 'Основные параметры',
|
|
29
|
+
'section.general_desc': 'Отображение валюты, курса пересчёта и часового пояса в интерфейсе',
|
|
30
|
+
'section.openrouter': 'Каталог цен OpenRouter',
|
|
31
|
+
'section.openrouter_desc': 'Публичный каталог моделей для автоматического определения ставок и окон скидок',
|
|
32
|
+
'section.diagnostics': 'Диагностика и статус',
|
|
33
|
+
'section.diagnostics_desc': 'Текущее состояние фоновой синхронизации и кэша тарифов',
|
|
34
|
+
'diag.models_count': 'Моделей в каталоге',
|
|
35
|
+
'diag.last_sync': 'Последняя синхронизация',
|
|
36
|
+
'diag.route': 'Активный маршрут',
|
|
37
|
+
'diag.tariff_source': 'Источник тарифа',
|
|
76
38
|
'field.currency': 'Символ валюты',
|
|
39
|
+
'hint.currency': 'Символ валюты для отображения расходов (например, $, ₽, €).',
|
|
77
40
|
'field.usdRate': 'Курс валюты к USD',
|
|
78
|
-
'
|
|
79
|
-
'
|
|
80
|
-
'hint.
|
|
81
|
-
'
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
marginLeft: 'auto', flex: 'none', color: 'var(--dsw-alias-label-tertiary)',
|
|
98
|
-
transition: 'transform .16s', transform: props.open ? 'rotate(180deg)' : 'none',
|
|
99
|
-
},
|
|
100
|
-
})
|
|
101
|
-
}
|
|
102
|
-
return React.createElement('svg', {
|
|
103
|
-
className: 'dcm-chev' + (props.open ? ' dcm-chev-open' : ''),
|
|
104
|
-
style: {
|
|
105
|
-
marginLeft: 'auto', flex: 'none', color: 'var(--dsw-alias-label-tertiary)',
|
|
106
|
-
transition: 'transform .16s', transform: props.open ? 'rotate(180deg)' : 'none',
|
|
107
|
-
},
|
|
108
|
-
width: 14, height: 14, viewBox: '0 0 14 14', fill: 'none', 'aria-hidden': 'true',
|
|
109
|
-
}, React.createElement('path', {
|
|
110
|
-
d: 'M3.5 5.25L7 8.75L10.5 5.25', stroke: 'currentColor', strokeWidth: 1.5,
|
|
111
|
-
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
112
|
-
}))
|
|
41
|
+
'hint.usdRate': 'Количество единиц вашей валюты за 1 USD (1 для долларов).',
|
|
42
|
+
'field.displayTimeZone': 'Временная зона (IANA)',
|
|
43
|
+
'hint.displayTimeZone': 'Временная зона для отображения пиковых окон (например, Europe/Moscow, UTC).',
|
|
44
|
+
'field.useOpenRouter': 'Использовать каталог OpenRouter',
|
|
45
|
+
'hint.useOpenRouter': 'Автоматически загружать ставки и скидочные окна для моделей через OpenRouter API.',
|
|
46
|
+
'field.refreshHours': 'Период обновления каталога (часов)',
|
|
47
|
+
'hint.refreshHours': 'Интервал фонового обновления каталога в часах (от 1 до 168).',
|
|
48
|
+
'settings.save': 'Сохранить настройки',
|
|
49
|
+
'settings.saving': 'Сохранение…',
|
|
50
|
+
'settings.saved': 'Настройки успешно сохранены',
|
|
51
|
+
'settings.saveFailed': 'Ошибка сохранения: ',
|
|
52
|
+
'settings.loading': 'Загрузка настроек…',
|
|
53
|
+
'settings.unavailable': 'Настройки временно недоступны',
|
|
54
|
+
'settings.retry': 'Повторить попытку',
|
|
55
|
+
chipUnknown: 'Тариф не определён',
|
|
56
|
+
routeUnknown: 'Маршрут не определён',
|
|
57
|
+
catalogUnavailable: 'Каталог недоступен: ',
|
|
58
|
+
peak: 'пик',
|
|
59
|
+
offpeak: 'не-пик',
|
|
113
60
|
}
|
|
114
61
|
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
'
|
|
118
|
-
'.
|
|
119
|
-
'.
|
|
120
|
-
'.
|
|
121
|
-
'.
|
|
122
|
-
'.
|
|
123
|
-
'.
|
|
124
|
-
'.
|
|
125
|
-
'.
|
|
126
|
-
'.
|
|
127
|
-
'.
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
62
|
+
const en = {
|
|
63
|
+
title: 'Cost Meter',
|
|
64
|
+
subtitle: 'Model rates, peak discount windows and session spend',
|
|
65
|
+
'header.title': 'Cost Meter',
|
|
66
|
+
'header.sub': 'Token spend monitoring, automatic tariff resolution and discount windows',
|
|
67
|
+
'badge.catalog_ok': 'Catalog: {count} models',
|
|
68
|
+
'badge.catalog_empty': 'Catalog: loading…',
|
|
69
|
+
'badge.catalog_off': 'Catalog: disabled',
|
|
70
|
+
'badge.catalog_err': 'Catalog: error',
|
|
71
|
+
'badge.deepseek': 'DeepSeek tariff: active',
|
|
72
|
+
'section.general': 'General Parameters',
|
|
73
|
+
'section.general_desc': 'Display currency, conversion rate and time zone configuration',
|
|
74
|
+
'section.openrouter': 'OpenRouter Pricing Catalog',
|
|
75
|
+
'section.openrouter_desc': 'Public catalog for dynamic model pricing and discount windows',
|
|
76
|
+
'section.diagnostics': 'Diagnostics & Status',
|
|
77
|
+
'section.diagnostics_desc': 'Current state of background synchronization and tariff cache',
|
|
78
|
+
'diag.models_count': 'Catalog models',
|
|
79
|
+
'diag.last_sync': 'Last sync',
|
|
80
|
+
'diag.route': 'Active route',
|
|
81
|
+
'diag.tariff_source': 'Tariff source',
|
|
82
|
+
'field.currency': 'Currency Symbol',
|
|
83
|
+
'hint.currency': 'Currency symbol shown in the UI (e.g. $, ₽, €).',
|
|
84
|
+
'field.usdRate': 'USD Exchange Rate',
|
|
85
|
+
'hint.usdRate': 'Units of currency per 1 USD (1 keeps USD).',
|
|
86
|
+
'field.displayTimeZone': 'Time Zone (IANA)',
|
|
87
|
+
'hint.displayTimeZone': 'IANA time zone for peak hours display (e.g. Europe/Moscow, UTC).',
|
|
88
|
+
'field.useOpenRouter': 'Use OpenRouter Catalog',
|
|
89
|
+
'hint.useOpenRouter': 'Pull rates and discount windows dynamically from public OpenRouter API.',
|
|
90
|
+
'field.refreshHours': 'Catalog Refresh Interval (hours)',
|
|
91
|
+
'hint.refreshHours': 'Background catalog fetch interval in hours (1 to 168).',
|
|
92
|
+
'settings.save': 'Save Settings',
|
|
93
|
+
'settings.saving': 'Saving…',
|
|
94
|
+
'settings.saved': 'Settings saved successfully',
|
|
95
|
+
'settings.saveFailed': 'Failed to save settings: ',
|
|
96
|
+
'settings.loading': 'Loading settings…',
|
|
97
|
+
'settings.unavailable': 'Settings temporarily unavailable',
|
|
98
|
+
'settings.retry': 'Retry',
|
|
99
|
+
chipUnknown: 'Unknown tariff',
|
|
100
|
+
routeUnknown: 'Unknown route',
|
|
101
|
+
catalogUnavailable: 'Catalog unavailable: ',
|
|
102
|
+
peak: 'peak',
|
|
103
|
+
offpeak: 'off-peak',
|
|
137
104
|
}
|
|
138
105
|
|
|
139
106
|
function makeT(dict, fallback) {
|
|
@@ -149,13 +116,197 @@ window.__ModuleLoader__.load({
|
|
|
149
116
|
}
|
|
150
117
|
|
|
151
118
|
function useActiveLocale(ctx) {
|
|
119
|
+
const localeService = (ctx && typeof ctx.get === 'function' ? ctx.get('locale') : null) || (ctx && ctx.locale) || (ctx && ctx.services && ctx.services.locale)
|
|
152
120
|
return React.useSyncExternalStore(
|
|
153
|
-
React.useMemo(() => (cb) => (
|
|
154
|
-
React.useCallback(() => (
|
|
121
|
+
React.useMemo(() => (cb) => (localeService && typeof localeService.subscribe === 'function' ? localeService.subscribe(cb) : () => {}), [localeService]),
|
|
122
|
+
React.useCallback(() => (localeService && typeof localeService.getSnapshot === 'function' ? localeService.getSnapshot().active : 'ru'), [localeService]),
|
|
155
123
|
React.useCallback(() => 'ru', []),
|
|
156
124
|
)
|
|
157
125
|
}
|
|
158
126
|
|
|
127
|
+
// ----------------------------------------------------------------- styles
|
|
128
|
+
|
|
129
|
+
const CSS = `
|
|
130
|
+
.dcm-card { border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-3); border-radius: 12px; list-style: none; margin-bottom: 12px; }
|
|
131
|
+
.dcm-head { appearance: none; width: 100%; font: inherit; color: inherit; text-align: left; cursor: pointer; background: 0 0; border: 0; border-radius: 12px; display: flex; align-items: center; gap: 12px; padding: 14px 16px; }
|
|
132
|
+
.dcm-title { color: var(--dsw-alias-label-primary); font-size: 15px; font-weight: 600; line-height: 1.4; }
|
|
133
|
+
.dcm-sub { color: var(--dsw-alias-label-secondary); font-size: 13px; }
|
|
134
|
+
.dcm-body { border-top: 1px solid var(--dsw-alias-border-l2); margin: 0 16px; padding: 16px 0 12px; display: flex; flex-direction: column; gap: 14px; }
|
|
135
|
+
|
|
136
|
+
.dcm-section-card { border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-2); border-radius: 10px; padding: 14px 16px; display: flex; flex-direction: column; gap: 12px; }
|
|
137
|
+
.dcm-section-title { font-size: 14px; font-weight: 600; color: var(--dsw-alias-label-primary); display: flex; align-items: center; justify-content: space-between; }
|
|
138
|
+
.dcm-section-desc { font-size: 12px; color: var(--dsw-alias-label-secondary); margin-top: -4px; line-height: 1.4; }
|
|
139
|
+
|
|
140
|
+
.dcm-grid-2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 12px; }
|
|
141
|
+
.dcm-field { display: flex; flex-direction: column; gap: 6px; }
|
|
142
|
+
.dcm-row-check { display: flex; align-items: center; gap: 10px; padding: 4px 0; }
|
|
143
|
+
.dcm-check { width: 16px; height: 16px; accent-color: var(--dsw-alias-state-brand-primary, #6366f1); cursor: pointer; }
|
|
144
|
+
.dcm-label { color: var(--dsw-alias-label-primary); font-size: 13px; font-weight: 500; }
|
|
145
|
+
.dcm-hint { color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 1.4; }
|
|
146
|
+
|
|
147
|
+
.dcm-input { height: 34px; border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-3); color: var(--dsw-alias-label-primary); border-radius: 8px; padding: 0 12px; font-size: 13px; width: 100%; box-sizing: border-box; }
|
|
148
|
+
.dcm-input:focus { outline: none; border-color: var(--dsw-alias-state-brand-primary, #6366f1); }
|
|
149
|
+
.dcm-input-err { border-color: var(--dsw-alias-state-error-primary, #ef4444); }
|
|
150
|
+
|
|
151
|
+
.dcm-badges { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin-top: 4px; }
|
|
152
|
+
.dcm-badge { font-size: 11px; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--dsw-alias-border-l2); display: inline-flex; align-items: center; gap: 4px; font-weight: 500; }
|
|
153
|
+
.dcm-badge-ok { border-color: var(--dsw-alias-state-success-primary); color: var(--dsw-alias-state-success-primary); background: rgba(16,185,129,0.08); }
|
|
154
|
+
.dcm-badge-warn { border-color: var(--dsw-alias-state-warning-primary); color: var(--dsw-alias-state-warning-primary); background: rgba(245,158,11,0.08); }
|
|
155
|
+
.dcm-badge-bad { border-color: var(--dsw-alias-state-error-primary); color: var(--dsw-alias-state-error-primary); background: rgba(239,68,68,0.08); }
|
|
156
|
+
|
|
157
|
+
.dcm-btn { appearance: none; font: inherit; cursor: pointer; border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px; padding: 6px 14px; font-size: 13px; background: var(--dsw-alias-bg-layer-3); color: var(--dsw-alias-label-primary); font-weight: 500; display: inline-flex; align-items: center; justify-content: center; gap: 6px; transition: all .15s ease; }
|
|
158
|
+
.dcm-btn:hover:not(:disabled) { background: var(--dsw-alias-bg-layer-4, var(--dsw-alias-bg-layer-3)); }
|
|
159
|
+
.dcm-btn-primary { background: var(--dsw-alias-label-primary); color: var(--dsw-alias-bg-layer-3); border-color: transparent; }
|
|
160
|
+
.dcm-btn-primary:hover:not(:disabled) { opacity: 0.9; }
|
|
161
|
+
.dcm-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
162
|
+
|
|
163
|
+
.dcm-alert { padding: 10px 14px; border-radius: 8px; font-size: 13px; line-height: 1.4; }
|
|
164
|
+
.dcm-alert-ok { background: rgba(16,185,129,0.1); color: var(--dsw-alias-state-success-primary); }
|
|
165
|
+
.dcm-alert-err { background: rgba(239,68,68,0.1); color: var(--dsw-alias-state-error-primary); }
|
|
166
|
+
.dcm-banner-warn { padding: 10px 14px; border-radius: 8px; background: rgba(245,158,11,0.1); color: var(--dsw-alias-state-warning-primary); font-size: 13px; }
|
|
167
|
+
|
|
168
|
+
.dcm-stat-box { padding: 10px 12px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px; background: var(--dsw-alias-bg-layer-3); display: flex; flex-direction: column; gap: 2px; }
|
|
169
|
+
.dcm-stat-val { font-size: 14px; font-weight: 600; color: var(--dsw-alias-label-primary); font-variant-numeric: tabular-nums; }
|
|
170
|
+
.dcm-stat-lbl { font-size: 11px; color: var(--dsw-alias-label-secondary); }
|
|
171
|
+
|
|
172
|
+
.dcm-foot { display: flex; justify-content: flex-end; align-items: center; gap: 10px; padding-top: 6px; }
|
|
173
|
+
|
|
174
|
+
.dcm-chip { display: inline-flex; align-items: center; gap: 6px; height: 22px; padding: 0 8px; border-radius: 6px; border: 1px solid transparent; cursor: pointer; font: inherit; font-size: 12px; font-variant-numeric: tabular-nums; line-height: 22px; transition: opacity .15s ease; }
|
|
175
|
+
.dcm-chip:hover { opacity: 0.88; }
|
|
176
|
+
|
|
177
|
+
.dcm-panel { position: absolute; top: 28px; right: 0; z-index: 40; width: 330px; padding: 14px 16px; border-radius: 12px; border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-base)); box-shadow: 0 10px 30px rgba(0,0,0,.28); font-size: 12px; line-height: 1.5; color: var(--dsw-alias-label-primary); text-align: left; font-variant-numeric: tabular-nums; }
|
|
178
|
+
.dcm-panel-title { font-weight: 600; padding-bottom: 4px; font-size: 13px; }
|
|
179
|
+
.dcm-panel-note { color: var(--dsw-alias-label-tertiary); font-size: 12px; }
|
|
180
|
+
.dcm-hr { height: 1px; background: var(--dsw-alias-border-l2); margin: 10px -16px; border: 0; }
|
|
181
|
+
.dcm-panel-grid { display: grid; grid-template-columns: 1fr 60px 60px; column-gap: 6px; align-items: center; }
|
|
182
|
+
.dcm-panel-grid-flat { display: grid; grid-template-columns: 1fr 72px; column-gap: 6px; align-items: center; }
|
|
183
|
+
.dcm-panel-grid-3 { display: grid; grid-template-columns: 1fr 58px 72px; column-gap: 6px; align-items: center; }
|
|
184
|
+
.dcm-th { color: var(--dsw-alias-label-tertiary); padding-bottom: 4px; font-size: 11px; }
|
|
185
|
+
.dcm-th-num { color: var(--dsw-alias-label-tertiary); padding-bottom: 4px; text-align: right; font-size: 11px; }
|
|
186
|
+
.dcm-td { padding: 3px 0; }
|
|
187
|
+
.dcm-cell-idle { text-align: right; padding: 3px 6px; border-radius: 4px; color: var(--dsw-alias-label-tertiary); }
|
|
188
|
+
.dcm-num { text-align: right; color: var(--dsw-alias-label-tertiary); padding: 3px 0; }
|
|
189
|
+
.dcm-cost { text-align: right; padding: 3px 0; }
|
|
190
|
+
.dcm-section-lbl { color: var(--dsw-alias-label-tertiary); padding-bottom: 4px; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
191
|
+
.dcm-total { display: flex; justify-content: space-between; font-weight: 600; font-size: 13px; }
|
|
192
|
+
`
|
|
193
|
+
|
|
194
|
+
function ensureStyles() {
|
|
195
|
+
if (typeof document === 'undefined') return
|
|
196
|
+
if (document.getElementById('dsh-cost-meter-unified-css')) return
|
|
197
|
+
const el = document.createElement('style')
|
|
198
|
+
el.id = 'dsh-cost-meter-unified-css'
|
|
199
|
+
el.dataset.dshPlugin = NS
|
|
200
|
+
el.textContent = CSS
|
|
201
|
+
document.head.appendChild(el)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function createErrorBoundary() {
|
|
205
|
+
if (!React || typeof React.Component !== 'function') {
|
|
206
|
+
return function NoopBoundary(props) { return props?.children || null }
|
|
207
|
+
}
|
|
208
|
+
return class ErrorBoundary extends React.Component {
|
|
209
|
+
constructor(props) {
|
|
210
|
+
super(props)
|
|
211
|
+
this.state = { hasError: false, error: null }
|
|
212
|
+
}
|
|
213
|
+
static getDerivedStateFromError(error) {
|
|
214
|
+
return { hasError: true, error }
|
|
215
|
+
}
|
|
216
|
+
componentDidCatch(error, errorInfo) {
|
|
217
|
+
console.error('[dsh-cost-meter] React error caught:', error, errorInfo)
|
|
218
|
+
}
|
|
219
|
+
render() {
|
|
220
|
+
if (this.state.hasError) {
|
|
221
|
+
return React.createElement(
|
|
222
|
+
'div',
|
|
223
|
+
{ className: 'dcm-alert dcm-alert-err', style: { margin: '8px 0' } },
|
|
224
|
+
React.createElement('div', { style: { fontWeight: 600, marginBottom: 4 } }, '⚠️ Cost Meter UI Error:'),
|
|
225
|
+
React.createElement('div', { style: { fontSize: 12, wordBreak: 'break-all' } }, String(this.state.error?.message || this.state.error)),
|
|
226
|
+
React.createElement(
|
|
227
|
+
'button',
|
|
228
|
+
{
|
|
229
|
+
type: 'button',
|
|
230
|
+
className: 'dcm-btn',
|
|
231
|
+
style: { marginTop: 8, fontSize: 12, padding: '3px 10px' },
|
|
232
|
+
onClick: () => this.setState({ hasError: false, error: null }),
|
|
233
|
+
},
|
|
234
|
+
'Retry'
|
|
235
|
+
)
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
return this.props?.children || null
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const ErrorBoundary = createErrorBoundary()
|
|
243
|
+
|
|
244
|
+
function refreshMirrorUntilVisible(ctx) {
|
|
245
|
+
const visible = () => {
|
|
246
|
+
try {
|
|
247
|
+
const s = (ctx?.get && ctx.get('lanSettings')) || (ctx?.get && ctx.get('settingsScope')) || ctx?.settingsScope
|
|
248
|
+
const view = s?.describe?.()?.getSnapshot?.()?.view
|
|
249
|
+
return !!view && Array.isArray(view.namespaces) && view.namespaces.some((row) => row.ns === NS)
|
|
250
|
+
} catch (_) {
|
|
251
|
+
return false
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (visible()) return () => {}
|
|
255
|
+
let tries = 0
|
|
256
|
+
const timer = setInterval(() => {
|
|
257
|
+
if (visible() || tries >= 15) { clearInterval(timer); return }
|
|
258
|
+
tries += 1
|
|
259
|
+
try {
|
|
260
|
+
const s = (ctx?.get && ctx.get('lanSettings')) || (ctx?.get && ctx.get('settingsScope')) || ctx?.settingsScope
|
|
261
|
+
s?.describe?.()?.load?.()
|
|
262
|
+
} catch (_) {}
|
|
263
|
+
}, 1000)
|
|
264
|
+
return () => clearInterval(timer)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function Chevron(props) {
|
|
268
|
+
return React.createElement(
|
|
269
|
+
'svg',
|
|
270
|
+
{
|
|
271
|
+
width: 16, height: 16, viewBox: '0 0 16 16', fill: 'none',
|
|
272
|
+
style: {
|
|
273
|
+
marginLeft: 'auto',
|
|
274
|
+
transform: props.open ? 'rotate(180deg)' : 'none',
|
|
275
|
+
transition: 'transform .18s ease',
|
|
276
|
+
color: 'var(--dsw-alias-label-secondary)',
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
React.createElement('path', {
|
|
280
|
+
d: 'M4 6L8 10L12 6',
|
|
281
|
+
stroke: 'currentColor',
|
|
282
|
+
strokeWidth: 1.6,
|
|
283
|
+
strokeLinecap: 'round',
|
|
284
|
+
strokeLinejoin: 'round',
|
|
285
|
+
}),
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function isValidTimeZone(tz) {
|
|
290
|
+
if (!tz || typeof tz !== 'string') return false
|
|
291
|
+
try {
|
|
292
|
+
new Intl.DateTimeFormat(undefined, { timeZone: tz })
|
|
293
|
+
return true
|
|
294
|
+
} catch {
|
|
295
|
+
return false
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function ago(timestamp) {
|
|
300
|
+
if (!timestamp) return 'нет данных'
|
|
301
|
+
const minutes = Math.max(0, Math.round((Date.now() - timestamp) / 60000))
|
|
302
|
+
if (minutes < 1) return 'только что'
|
|
303
|
+
if (minutes < 60) return minutes + ' мин назад'
|
|
304
|
+
const hours = Math.round(minutes / 60)
|
|
305
|
+
return hours < 48 ? hours + ' ч назад' : Math.round(hours / 24) + ' дн назад'
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ---------------------------------------------------------------- card
|
|
309
|
+
|
|
159
310
|
function CostMeterCard(props) {
|
|
160
311
|
const ctx = props.ctx
|
|
161
312
|
const locale = useActiveLocale(ctx)
|
|
@@ -165,10 +316,12 @@ window.__ModuleLoader__.load({
|
|
|
165
316
|
const [saving, setSaving] = React.useState(false)
|
|
166
317
|
const [err, setErr] = React.useState('')
|
|
167
318
|
const [saved, setSaved] = React.useState(false)
|
|
319
|
+
const [diagState, setDiagState] = React.useState(null)
|
|
168
320
|
|
|
321
|
+
const settingsScope = (ctx && typeof ctx.get === 'function' ? ctx.get('settingsScope') : null) || (ctx && ctx.settingsScope) || (ctx && ctx.services && ctx.services.settingsScope)
|
|
169
322
|
const scope = React.useMemo(
|
|
170
|
-
() => (
|
|
171
|
-
[
|
|
323
|
+
() => (settingsScope ? settingsScope.bind({ namespace: NS }) : undefined),
|
|
324
|
+
[settingsScope],
|
|
172
325
|
)
|
|
173
326
|
|
|
174
327
|
const snapshot = React.useSyncExternalStore(
|
|
@@ -177,7 +330,16 @@ window.__ModuleLoader__.load({
|
|
|
177
330
|
React.useCallback(() => ({ status: 'loading' }), []),
|
|
178
331
|
)
|
|
179
332
|
|
|
180
|
-
React.useEffect(() => {
|
|
333
|
+
React.useEffect(() => { ensureStyles() }, [])
|
|
334
|
+
|
|
335
|
+
// Poll state for live diagnostics
|
|
336
|
+
React.useEffect(() => {
|
|
337
|
+
if (!open) return
|
|
338
|
+
fetch(STATE_PATH, { headers: { accept: 'application/json' } })
|
|
339
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
340
|
+
.then((data) => { if (data) setDiagState(data) })
|
|
341
|
+
.catch(() => {})
|
|
342
|
+
}, [open])
|
|
181
343
|
|
|
182
344
|
const status = (snapshot && snapshot.status) || 'loading'
|
|
183
345
|
const stored = (snapshot && snapshot.value) || {}
|
|
@@ -188,14 +350,21 @@ window.__ModuleLoader__.load({
|
|
|
188
350
|
currency: stored.currency !== undefined ? String(stored.currency) : '$',
|
|
189
351
|
usdRate: stored.usdRate !== undefined ? String(stored.usdRate) : '1',
|
|
190
352
|
displayTimeZone: stored.displayTimeZone !== undefined ? String(stored.displayTimeZone) : 'Europe/Moscow',
|
|
353
|
+
useOpenRouter: stored.useOpenRouter !== undefined ? Boolean(stored.useOpenRouter) : true,
|
|
354
|
+
refreshHours: stored.refreshHours !== undefined ? String(stored.refreshHours) : '24',
|
|
191
355
|
})
|
|
192
356
|
}
|
|
193
357
|
}, [status, stored, draft])
|
|
194
358
|
|
|
359
|
+
const tzValid = draft ? isValidTimeZone(draft.displayTimeZone) : true
|
|
360
|
+
const rateValid = draft ? (!isNaN(Number(draft.usdRate)) && Number(draft.usdRate) > 0) : true
|
|
361
|
+
const hoursValid = draft ? (!isNaN(Number(draft.refreshHours)) && Number(draft.refreshHours) >= 1 && Number(draft.refreshHours) <= 168) : true
|
|
362
|
+
|
|
195
363
|
const save = async () => {
|
|
196
364
|
if (!scope || !draft) return
|
|
197
365
|
setSaving(true); setErr(''); setSaved(false)
|
|
198
366
|
const broken = []
|
|
367
|
+
|
|
199
368
|
try {
|
|
200
369
|
await scope.set('currency', String(draft.currency || '$'))
|
|
201
370
|
} catch (e) { broken.push('currency: ' + (e && e.message || String(e))) }
|
|
@@ -218,6 +387,19 @@ window.__ModuleLoader__.load({
|
|
|
218
387
|
} catch (e) { broken.push('displayTimeZone: ' + (e && e.message || String(e))) }
|
|
219
388
|
}
|
|
220
389
|
|
|
390
|
+
try {
|
|
391
|
+
await scope.set('useOpenRouter', Boolean(draft.useOpenRouter))
|
|
392
|
+
} catch (e) { broken.push('useOpenRouter: ' + (e && e.message || String(e))) }
|
|
393
|
+
|
|
394
|
+
const hoursNum = Number(String(draft.refreshHours).trim())
|
|
395
|
+
if (!Number.isFinite(hoursNum) || hoursNum < 1 || hoursNum > 168) {
|
|
396
|
+
broken.push('refreshHours: must be between 1 and 168 hours')
|
|
397
|
+
} else {
|
|
398
|
+
try {
|
|
399
|
+
await scope.set('refreshHours', hoursNum)
|
|
400
|
+
} catch (e) { broken.push('refreshHours: ' + (e && e.message || String(e))) }
|
|
401
|
+
}
|
|
402
|
+
|
|
221
403
|
setSaving(false)
|
|
222
404
|
if (broken.length) {
|
|
223
405
|
setErr(t('settings.saveFailed') + broken.join('; '))
|
|
@@ -227,59 +409,152 @@ window.__ModuleLoader__.load({
|
|
|
227
409
|
setTimeout(() => setSaved(false), 2500)
|
|
228
410
|
}
|
|
229
411
|
|
|
412
|
+
const modelsCount = (diagState && typeof diagState.catalogModelsCount === 'number')
|
|
413
|
+
? diagState.catalogModelsCount
|
|
414
|
+
: ((diagState && diagState.schedules) ? Object.keys(diagState.schedules).length : null)
|
|
415
|
+
const catalogHasErr = diagState && Boolean(diagState.catalogError)
|
|
416
|
+
const openRouterEnabled = draft ? Boolean(draft.useOpenRouter) : true
|
|
417
|
+
|
|
230
418
|
return React.createElement('li', { className: 'dcm-card' },
|
|
231
419
|
React.createElement('button', {
|
|
232
420
|
type: 'button', className: 'dcm-head', 'aria-expanded': open,
|
|
233
421
|
onClick: () => setOpen((v) => !v),
|
|
234
422
|
},
|
|
235
|
-
React.createElement('
|
|
236
|
-
React.createElement('span', { className: 'dcm-title' }, t('title')),
|
|
237
|
-
React.createElement('span', { className: 'dcm-sub' }, t('
|
|
423
|
+
React.createElement('div', { style: { flex: 1, display: 'flex', flexDirection: 'column', gap: 2 } },
|
|
424
|
+
React.createElement('span', { className: 'dcm-title' }, t('header.title')),
|
|
425
|
+
React.createElement('span', { className: 'dcm-sub' }, t('header.sub')),
|
|
426
|
+
React.createElement('div', { className: 'dcm-badges' },
|
|
427
|
+
React.createElement('span', { className: 'dcm-badge dcm-badge-ok' }, t('badge.deepseek')),
|
|
428
|
+
openRouterEnabled
|
|
429
|
+
? (catalogHasErr
|
|
430
|
+
? React.createElement('span', { className: 'dcm-badge dcm-badge-bad' }, t('badge.catalog_err'))
|
|
431
|
+
: (modelsCount !== null && modelsCount > 0
|
|
432
|
+
? React.createElement('span', { className: 'dcm-badge dcm-badge-ok' }, t('badge.catalog_ok', { count: modelsCount }))
|
|
433
|
+
: React.createElement('span', { className: 'dcm-badge dcm-badge-warn' }, t('badge.catalog_empty'))))
|
|
434
|
+
: React.createElement('span', { className: 'dcm-badge dcm-badge-warn' }, t('badge.catalog_off')),
|
|
435
|
+
draft && draft.displayTimeZone
|
|
436
|
+
? React.createElement('span', { className: 'dcm-badge' }, draft.displayTimeZone)
|
|
437
|
+
: null,
|
|
438
|
+
),
|
|
238
439
|
),
|
|
239
440
|
React.createElement(Chevron, { open }),
|
|
240
441
|
),
|
|
241
442
|
open ? React.createElement('div', { className: 'dcm-body' },
|
|
242
443
|
status === 'loading'
|
|
243
|
-
? React.createElement('
|
|
444
|
+
? React.createElement('div', { className: 'dcm-hint', style: { padding: '12px 0' } }, t('settings.loading'))
|
|
244
445
|
: status !== 'ready'
|
|
245
|
-
? React.createElement('
|
|
246
|
-
: React.createElement(
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
React.createElement('
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
446
|
+
? React.createElement('div', { className: 'dcm-alert dcm-alert-err' }, t('settings.unavailable'))
|
|
447
|
+
: React.createElement(React.Fragment, null,
|
|
448
|
+
// Section 1: General Parameters
|
|
449
|
+
React.createElement('div', { className: 'dcm-section-card' },
|
|
450
|
+
React.createElement('div', { className: 'dcm-section-title' }, t('section.general')),
|
|
451
|
+
React.createElement('div', { className: 'dcm-section-desc' }, t('section.general_desc')),
|
|
452
|
+
React.createElement('div', { className: 'dcm-grid-2' },
|
|
453
|
+
React.createElement('div', { className: 'dcm-field' },
|
|
454
|
+
React.createElement('label', { className: 'dcm-label' }, t('field.currency')),
|
|
455
|
+
React.createElement('input', {
|
|
456
|
+
className: 'dcm-input',
|
|
457
|
+
value: (draft && draft.currency) !== undefined ? draft.currency : '',
|
|
458
|
+
onChange: (e) => setDraft({ ...draft, currency: e.target.value }),
|
|
459
|
+
}),
|
|
460
|
+
React.createElement('span', { className: 'dcm-hint' }, t('hint.currency')),
|
|
461
|
+
),
|
|
462
|
+
React.createElement('div', { className: 'dcm-field' },
|
|
463
|
+
React.createElement('label', { className: 'dcm-label' }, t('field.usdRate')),
|
|
464
|
+
React.createElement('input', {
|
|
465
|
+
className: 'dcm-input' + (rateValid ? '' : ' dcm-input-err'),
|
|
466
|
+
type: 'number',
|
|
467
|
+
step: 'any',
|
|
468
|
+
value: (draft && draft.usdRate) !== undefined ? draft.usdRate : '',
|
|
469
|
+
onChange: (e) => setDraft({ ...draft, usdRate: e.target.value }),
|
|
470
|
+
}),
|
|
471
|
+
React.createElement('span', { className: 'dcm-hint' }, t('hint.usdRate')),
|
|
472
|
+
),
|
|
473
|
+
),
|
|
474
|
+
React.createElement('div', { className: 'dcm-field' },
|
|
475
|
+
React.createElement('label', { className: 'dcm-label' }, t('field.displayTimeZone')),
|
|
476
|
+
React.createElement('input', {
|
|
477
|
+
className: 'dcm-input' + (tzValid ? '' : ' dcm-input-err'),
|
|
478
|
+
value: (draft && draft.displayTimeZone) !== undefined ? draft.displayTimeZone : '',
|
|
479
|
+
onChange: (e) => setDraft({ ...draft, displayTimeZone: e.target.value }),
|
|
480
|
+
}),
|
|
481
|
+
React.createElement('span', { className: 'dcm-hint' },
|
|
482
|
+
tzValid
|
|
483
|
+
? t('hint.displayTimeZone')
|
|
484
|
+
: 'Некорректный идентификатор временной зоны IANA'
|
|
485
|
+
),
|
|
486
|
+
),
|
|
255
487
|
),
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
488
|
+
|
|
489
|
+
// Section 2: OpenRouter Pricing Catalog
|
|
490
|
+
React.createElement('div', { className: 'dcm-section-card' },
|
|
491
|
+
React.createElement('div', { className: 'dcm-section-title' }, t('section.openrouter')),
|
|
492
|
+
React.createElement('div', { className: 'dcm-section-desc' }, t('section.openrouter_desc')),
|
|
493
|
+
React.createElement('div', { className: 'dcm-row-check' },
|
|
494
|
+
React.createElement('input', {
|
|
495
|
+
id: 'dcm-use-openrouter',
|
|
496
|
+
type: 'checkbox',
|
|
497
|
+
className: 'dcm-check',
|
|
498
|
+
checked: draft ? Boolean(draft.useOpenRouter) : true,
|
|
499
|
+
onChange: (e) => setDraft({ ...draft, useOpenRouter: e.target.checked }),
|
|
500
|
+
}),
|
|
501
|
+
React.createElement('label', { htmlFor: 'dcm-use-openrouter', className: 'dcm-label', style: { cursor: 'pointer' } }, t('field.useOpenRouter')),
|
|
502
|
+
),
|
|
503
|
+
React.createElement('span', { className: 'dcm-hint', style: { marginTop: -6 } }, t('hint.useOpenRouter')),
|
|
504
|
+
React.createElement('div', { className: 'dcm-field', style: { marginTop: 4 } },
|
|
505
|
+
React.createElement('label', { className: 'dcm-label' }, t('field.refreshHours')),
|
|
506
|
+
React.createElement('input', {
|
|
507
|
+
className: 'dcm-input' + (hoursValid ? '' : ' dcm-input-err'),
|
|
508
|
+
type: 'number',
|
|
509
|
+
min: 1,
|
|
510
|
+
max: 168,
|
|
511
|
+
value: (draft && draft.refreshHours) !== undefined ? draft.refreshHours : '',
|
|
512
|
+
onChange: (e) => setDraft({ ...draft, refreshHours: e.target.value }),
|
|
513
|
+
}),
|
|
514
|
+
React.createElement('span', { className: 'dcm-hint' }, t('hint.refreshHours')),
|
|
515
|
+
),
|
|
266
516
|
),
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
517
|
+
|
|
518
|
+
// Section 3: Live Diagnostics
|
|
519
|
+
React.createElement('div', { className: 'dcm-section-card' },
|
|
520
|
+
React.createElement('div', { className: 'dcm-section-title' }, t('section.diagnostics')),
|
|
521
|
+
React.createElement('div', { className: 'dcm-section-desc' }, t('section.diagnostics_desc')),
|
|
522
|
+
React.createElement('div', { className: 'dcm-grid-2' },
|
|
523
|
+
React.createElement('div', { className: 'dcm-stat-box' },
|
|
524
|
+
React.createElement('span', { className: 'dcm-stat-val' },
|
|
525
|
+
modelsCount !== null ? String(modelsCount) : '—'
|
|
526
|
+
),
|
|
527
|
+
React.createElement('span', { className: 'dcm-stat-lbl' }, t('diag.models_count')),
|
|
528
|
+
),
|
|
529
|
+
React.createElement('div', { className: 'dcm-stat-box' },
|
|
530
|
+
React.createElement('span', { className: 'dcm-stat-val' },
|
|
531
|
+
diagState ? ago(diagState.fetchedAt) : '—'
|
|
532
|
+
),
|
|
533
|
+
React.createElement('span', { className: 'dcm-stat-lbl' }, t('diag.last_sync')),
|
|
534
|
+
),
|
|
535
|
+
React.createElement('div', { className: 'dcm-stat-box' },
|
|
536
|
+
React.createElement('span', { className: 'dcm-stat-val', style: { fontSize: 12 } },
|
|
537
|
+
diagState && diagState.route ? (diagState.route.provider + '/' + diagState.route.model) : 'default'
|
|
538
|
+
),
|
|
539
|
+
React.createElement('span', { className: 'dcm-stat-lbl' }, t('diag.route')),
|
|
540
|
+
),
|
|
541
|
+
React.createElement('div', { className: 'dcm-stat-box' },
|
|
542
|
+
React.createElement('span', { className: 'dcm-stat-val', style: { fontSize: 12 } },
|
|
543
|
+
diagState && diagState.source ? String(diagState.source).toUpperCase() : 'DEEPSEEK'
|
|
544
|
+
),
|
|
545
|
+
React.createElement('span', { className: 'dcm-stat-lbl' }, t('diag.tariff_source')),
|
|
546
|
+
),
|
|
547
|
+
),
|
|
275
548
|
),
|
|
276
|
-
|
|
277
|
-
|
|
549
|
+
|
|
550
|
+
// Alerts & Action Footer
|
|
551
|
+
err ? React.createElement('div', { className: 'dcm-alert dcm-alert-err' }, err) : null,
|
|
552
|
+
saved ? React.createElement('div', { className: 'dcm-alert dcm-alert-ok' }, t('settings.saved')) : null,
|
|
278
553
|
React.createElement('div', { className: 'dcm-foot' },
|
|
279
554
|
React.createElement('button', {
|
|
280
555
|
type: 'button',
|
|
281
|
-
className: 'dcm-
|
|
282
|
-
disabled: saving,
|
|
556
|
+
className: 'dcm-btn dcm-btn-primary',
|
|
557
|
+
disabled: saving || !tzValid || !rateValid || !hoursValid,
|
|
283
558
|
onClick: save,
|
|
284
559
|
}, saving ? t('settings.saving') : t('settings.save')),
|
|
285
560
|
),
|
|
@@ -288,21 +563,18 @@ window.__ModuleLoader__.load({
|
|
|
288
563
|
)
|
|
289
564
|
}
|
|
290
565
|
|
|
291
|
-
|
|
292
566
|
// ---------------------------------------------------------------- tariff
|
|
293
567
|
|
|
294
568
|
function inWindow(minute, w) {
|
|
295
569
|
return w.start <= w.end ? minute >= w.start && minute < w.end : minute >= w.start || minute < w.end
|
|
296
570
|
}
|
|
297
571
|
|
|
298
|
-
/** Rates in force at a given UTC minute: the last matching window, else base. */
|
|
299
572
|
function ratesAt(schedule, minute) {
|
|
300
573
|
const windows = (schedule && schedule.windows) || []
|
|
301
574
|
for (const w of windows) if (inWindow(minute, w)) return w.rates
|
|
302
575
|
return (schedule && schedule.base) || { cacheHit: 0, input: 0, cacheWrite: 0, output: 0 }
|
|
303
576
|
}
|
|
304
577
|
|
|
305
|
-
/** Minutes until the rate card changes, or null when it never does. */
|
|
306
578
|
function minutesUntilChange(schedule, minute) {
|
|
307
579
|
const now = ratesAt(schedule, minute)
|
|
308
580
|
for (let step = 1; step <= DAY; step += 1) {
|
|
@@ -312,10 +584,6 @@ window.__ModuleLoader__.load({
|
|
|
312
584
|
return null
|
|
313
585
|
}
|
|
314
586
|
|
|
315
|
-
/**
|
|
316
|
-
* The day's two extremes, by output rate: what the widget labels
|
|
317
|
-
* "не-пик" and "пик". Equal extremes mean a flat tariff.
|
|
318
|
-
*/
|
|
319
587
|
function extremes(schedule) {
|
|
320
588
|
let low = null
|
|
321
589
|
let high = null
|
|
@@ -327,7 +595,6 @@ window.__ModuleLoader__.load({
|
|
|
327
595
|
return { low: low || {}, high: high || {}, flat: !low || !high || low.output === high.output }
|
|
328
596
|
}
|
|
329
597
|
|
|
330
|
-
/** Total tokens across every half-hour slot of one model. */
|
|
331
598
|
function sumSlots(slots) {
|
|
332
599
|
const total = { uncachedInputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 }
|
|
333
600
|
for (const key of Object.keys(slots || {})) {
|
|
@@ -337,11 +604,6 @@ window.__ModuleLoader__.load({
|
|
|
337
604
|
return total
|
|
338
605
|
}
|
|
339
606
|
|
|
340
|
-
/**
|
|
341
|
-
* Cost of one model, each half-hour slot charged at the rate that applied
|
|
342
|
-
* in that slot. Slot 14 is 07:00–07:30 UTC, so its midpoint (07:15) picks
|
|
343
|
-
* the window without landing on a boundary.
|
|
344
|
-
*/
|
|
345
607
|
function spendBySlots(slots, schedule) {
|
|
346
608
|
let usd = 0
|
|
347
609
|
for (const key of Object.keys(slots || {})) {
|
|
@@ -351,7 +613,6 @@ window.__ModuleLoader__.load({
|
|
|
351
613
|
return usd
|
|
352
614
|
}
|
|
353
615
|
|
|
354
|
-
/** Session spend in USD for one rate card. */
|
|
355
616
|
function spendUsd(usage, rates) {
|
|
356
617
|
const u = usage || {}
|
|
357
618
|
const perMillion =
|
|
@@ -362,16 +623,15 @@ window.__ModuleLoader__.load({
|
|
|
362
623
|
return perMillion / 1e6
|
|
363
624
|
}
|
|
364
625
|
|
|
365
|
-
// --------------------------------------------------------------- format
|
|
366
|
-
|
|
367
626
|
function rate(value) {
|
|
368
627
|
if (!value) return '—'
|
|
369
628
|
const text = value < 0.1 ? value.toFixed(3) : value.toFixed(2)
|
|
370
|
-
return '$' + text.replace(/(
|
|
629
|
+
return '$' + text.replace(/(\\.\\d*?)0+$/, '$1').replace(/\\.$/, '')
|
|
371
630
|
}
|
|
372
631
|
|
|
373
632
|
function money(value, currency) {
|
|
374
|
-
|
|
633
|
+
const n = Number(value) || 0
|
|
634
|
+
return currency + n.toFixed(Math.abs(n) >= 100 ? 0 : 2)
|
|
375
635
|
}
|
|
376
636
|
|
|
377
637
|
function tokens(value) {
|
|
@@ -390,20 +650,6 @@ window.__ModuleLoader__.load({
|
|
|
390
650
|
return String(Math.floor(minute / 60)).padStart(2, '0') + ':' + String(minute % 60).padStart(2, '0')
|
|
391
651
|
}
|
|
392
652
|
|
|
393
|
-
function isValidTimeZone(tz) {
|
|
394
|
-
if (!tz || typeof tz !== 'string') return false
|
|
395
|
-
try {
|
|
396
|
-
new Intl.DateTimeFormat(undefined, { timeZone: tz })
|
|
397
|
-
return true
|
|
398
|
-
} catch {
|
|
399
|
-
return false
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
/**
|
|
404
|
-
* Render a UTC minute-of-day in the display zone. Built from a real Date so
|
|
405
|
-
* the zone's current offset (and any DST) comes from Intl, not arithmetic.
|
|
406
|
-
*/
|
|
407
653
|
function hhmmInZone(minute, zone, reference) {
|
|
408
654
|
const d = new Date(Date.UTC(
|
|
409
655
|
reference.getUTCFullYear(), reference.getUTCMonth(), reference.getUTCDate(), 0, minute,
|
|
@@ -417,7 +663,6 @@ window.__ModuleLoader__.load({
|
|
|
417
663
|
}
|
|
418
664
|
}
|
|
419
665
|
|
|
420
|
-
/** Short zone label for the panel: "МСК" for Moscow, else the city part. */
|
|
421
666
|
function zoneLabel(zone) {
|
|
422
667
|
if (!zone) return 'UTC'
|
|
423
668
|
if (zone === 'Europe/Moscow') return 'МСК'
|
|
@@ -425,56 +670,7 @@ window.__ModuleLoader__.load({
|
|
|
425
670
|
return city.replace(/_/g, ' ')
|
|
426
671
|
}
|
|
427
672
|
|
|
428
|
-
|
|
429
|
-
if (!timestamp) return 'нет данных'
|
|
430
|
-
const minutes = Math.max(0, Math.round((Date.now() - timestamp) / 60000))
|
|
431
|
-
if (minutes < 60) return minutes + ' мин назад'
|
|
432
|
-
const hours = Math.round(minutes / 60)
|
|
433
|
-
return hours < 48 ? hours + ' ч назад' : Math.round(hours / 24) + ' дн назад'
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
// ---------------------------------------------------------------- styles
|
|
437
|
-
|
|
438
|
-
const C = {
|
|
439
|
-
label: 'var(--dsw-alias-label-primary)',
|
|
440
|
-
muted: 'var(--dsw-alias-label-tertiary)',
|
|
441
|
-
line: 'var(--dsw-alias-border-l2)',
|
|
442
|
-
panelBg: 'var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-base))',
|
|
443
|
-
okFg: 'var(--dsw-alias-state-success-primary)',
|
|
444
|
-
okBg: 'var(--dsw-alias-state-success-tertiary)',
|
|
445
|
-
warnFg: 'var(--dsw-alias-state-warn-primary)',
|
|
446
|
-
warnBg: 'var(--dsw-alias-state-warn-tertiary)',
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
const S = {
|
|
450
|
-
chip: {
|
|
451
|
-
display: 'inline-flex', alignItems: 'center', gap: 6, height: 22, padding: '0 8px',
|
|
452
|
-
borderRadius: 6, border: 'none', cursor: 'pointer', font: 'inherit', fontSize: 12,
|
|
453
|
-
fontVariantNumeric: 'tabular-nums', lineHeight: '22px',
|
|
454
|
-
},
|
|
455
|
-
panel: {
|
|
456
|
-
position: 'absolute', top: 28, right: 0, zIndex: 40, width: 320, padding: '12px 14px',
|
|
457
|
-
borderRadius: 10, border: '1px solid ' + C.line, background: C.panelBg,
|
|
458
|
-
boxShadow: '0 10px 30px rgba(0,0,0,.28)', fontSize: 12, lineHeight: 1.5,
|
|
459
|
-
color: C.label, textAlign: 'left', fontVariantNumeric: 'tabular-nums',
|
|
460
|
-
},
|
|
461
|
-
title: { fontWeight: 600, paddingBottom: 4 },
|
|
462
|
-
note: { color: C.muted },
|
|
463
|
-
hr: { height: 1, background: C.line, margin: '10px -14px' },
|
|
464
|
-
grid: { display: 'grid', gridTemplateColumns: '1fr 60px 60px', columnGap: 6, alignItems: 'center' },
|
|
465
|
-
gridFlat: { display: 'grid', gridTemplateColumns: '1fr 72px', columnGap: 6, alignItems: 'center' },
|
|
466
|
-
grid3: { display: 'grid', gridTemplateColumns: '1fr 58px 72px', columnGap: 6, alignItems: 'center' },
|
|
467
|
-
th: { color: C.muted, paddingBottom: 4 },
|
|
468
|
-
thNum: { color: C.muted, paddingBottom: 4, textAlign: 'right' },
|
|
469
|
-
td: { padding: '3px 0' },
|
|
470
|
-
cellIdle: { textAlign: 'right', padding: '3px 6px', borderRadius: 4, color: C.muted },
|
|
471
|
-
num: { textAlign: 'right', color: C.muted, padding: '3px 0' },
|
|
472
|
-
cost: { textAlign: 'right', padding: '3px 0' },
|
|
473
|
-
section: { color: C.muted, paddingBottom: 4 },
|
|
474
|
-
total: { display: 'flex', justifyContent: 'space-between', fontWeight: 600 },
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
// ------------------------------------------------------------ component
|
|
673
|
+
// ------------------------------------------------------------ CostMeter
|
|
478
674
|
|
|
479
675
|
function CostMeter(props) {
|
|
480
676
|
const ctx = props.ctx
|
|
@@ -487,7 +683,10 @@ window.__ModuleLoader__.load({
|
|
|
487
683
|
const [open, setOpen] = React.useState(false)
|
|
488
684
|
const [now, setNow] = React.useState(() => new Date())
|
|
489
685
|
const box = React.useRef(null)
|
|
490
|
-
|
|
686
|
+
|
|
687
|
+
React.useEffect(() => { ensureStyles() }, [])
|
|
688
|
+
|
|
689
|
+
const settingsScope = (ctx && typeof ctx.get === 'function' ? ctx.get('settingsScope') : null) || (ctx && ctx.settingsScope) || (ctx && ctx.services && ctx.services.settingsScope)
|
|
491
690
|
const settingsSnap = React.useSyncExternalStore
|
|
492
691
|
? React.useSyncExternalStore(
|
|
493
692
|
(cb) => (settingsScope && typeof settingsScope.subscribe === 'function' ? settingsScope.subscribe(cb) : () => {}),
|
|
@@ -496,10 +695,6 @@ window.__ModuleLoader__.load({
|
|
|
496
695
|
)
|
|
497
696
|
: null
|
|
498
697
|
|
|
499
|
-
// Every model this session actually billed, so each part is priced on its
|
|
500
|
-
// own tariff instead of the current model's. Usage arrives split into
|
|
501
|
-
// half-hour slots of the UTC day — spend is charged at the rate that was
|
|
502
|
-
// in force then, so a tariff switch never reprices what is already spent.
|
|
503
698
|
const used = (byModel && Array.isArray(byModel.models) ? byModel.models : [])
|
|
504
699
|
.map((m) => ({ ...m, usage: sumSlots(m && m.slots) }))
|
|
505
700
|
.filter((m) => m.usage.uncachedInputTokens || m.usage.outputTokens ||
|
|
@@ -518,8 +713,7 @@ window.__ModuleLoader__.load({
|
|
|
518
713
|
})
|
|
519
714
|
.catch(() => {})
|
|
520
715
|
}, [])
|
|
521
|
-
|
|
522
|
-
// own tariff before its tokens can be priced.
|
|
716
|
+
|
|
523
717
|
React.useEffect(() => { load(usedKeys) }, [load, usedKeys])
|
|
524
718
|
React.useEffect(() => { if (open) load(usedKeys) }, [open, load, usedKeys])
|
|
525
719
|
React.useEffect(() => {
|
|
@@ -542,18 +736,18 @@ window.__ModuleLoader__.load({
|
|
|
542
736
|
const hasUsage = usage !== undefined && usage !== null
|
|
543
737
|
const minute = now.getUTCHours() * 60 + now.getUTCMinutes()
|
|
544
738
|
|
|
545
|
-
// No rate card for this route: say so instead of showing a fake zero.
|
|
546
739
|
if (!schedule) {
|
|
547
740
|
return h('div', { ref: box, style: { position: 'relative' } }, [
|
|
548
741
|
h('button', {
|
|
549
742
|
key: 'chip', type: 'button',
|
|
550
|
-
|
|
743
|
+
className: 'dcm-chip',
|
|
744
|
+
style: { background: 'transparent', color: 'var(--dsw-alias-label-tertiary)' },
|
|
551
745
|
onClick: () => setOpen((v) => !v), title: t('chipUnknown'),
|
|
552
746
|
}, 'тариф ?'),
|
|
553
|
-
open ? h('div', { key: 'panel',
|
|
554
|
-
h('div', { key: 't',
|
|
747
|
+
open ? h('div', { key: 'panel', className: 'dcm-panel' }, [
|
|
748
|
+
h('div', { key: 't', className: 'dcm-panel-title' },
|
|
555
749
|
state.route ? state.route.provider + ' / ' + state.route.model : t('routeUnknown')),
|
|
556
|
-
h('div', { key: 'n',
|
|
750
|
+
h('div', { key: 'n', className: 'dcm-panel-note' },
|
|
557
751
|
state.catalogError
|
|
558
752
|
? t('catalogUnavailable') + state.catalogError
|
|
559
753
|
: 'Модель не найдена в каталоге OpenRouter. Задайте ставки вручную в настройках (prices) или сопоставьте модель через modelMap.'),
|
|
@@ -566,9 +760,6 @@ window.__ModuleLoader__.load({
|
|
|
566
760
|
const cheapNow = !flat && active.output === low.output
|
|
567
761
|
const left = clock(minutesUntilChange(schedule, minute))
|
|
568
762
|
|
|
569
|
-
// Per-model spend: each model on its own tariff, at that tariff's rate
|
|
570
|
-
// for the current moment. Falls back to the session-wide totals when the
|
|
571
|
-
// per-model projection is unavailable (older harness, headless).
|
|
572
763
|
const schedules = state.schedules || {}
|
|
573
764
|
const perModel = used.map((m) => {
|
|
574
765
|
const key = m.provider + '/' + m.model
|
|
@@ -586,27 +777,22 @@ window.__ModuleLoader__.load({
|
|
|
586
777
|
const total = perModel.length
|
|
587
778
|
? perModel.reduce((sum, m) => sum + m.cost, 0)
|
|
588
779
|
: spendUsd(usage, active) * fx
|
|
589
|
-
const unpriced = perModel.filter((m) => !m.known)
|
|
590
780
|
|
|
591
|
-
const fg = cheapNow ?
|
|
592
|
-
const bg = cheapNow ?
|
|
781
|
+
const fg = cheapNow ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-state-warning-primary)'
|
|
782
|
+
const bg = cheapNow ? 'rgba(16,185,129,0.12)' : 'rgba(245,158,11,0.12)'
|
|
593
783
|
const activeCell = { textAlign: 'right', padding: '3px 6px', borderRadius: 4, background: bg, color: fg, fontWeight: 600 }
|
|
594
784
|
|
|
595
|
-
// Two tiers: cheap and full side by side, the active one filled.
|
|
596
|
-
// One tier: a single plain column, no highlight to explain.
|
|
597
785
|
const rateRow = (label, key) => flat
|
|
598
786
|
? [
|
|
599
|
-
h('div', { key: label + '-l',
|
|
600
|
-
h('div', { key: label + '-v',
|
|
787
|
+
h('div', { key: label + '-l', className: 'dcm-td' }, label),
|
|
788
|
+
h('div', { key: label + '-v', className: 'dcm-cost' }, rate(active[key])),
|
|
601
789
|
]
|
|
602
790
|
: [
|
|
603
|
-
h('div', { key: label + '-l',
|
|
604
|
-
h('div', { key: label + '-lo', style: cheapNow ? activeCell :
|
|
605
|
-
h('div', { key: label + '-hi', style: cheapNow ?
|
|
791
|
+
h('div', { key: label + '-l', className: 'dcm-td' }, label),
|
|
792
|
+
h('div', { key: label + '-lo', style: cheapNow ? activeCell : undefined, className: cheapNow ? '' : 'dcm-cell-idle' }, rate(low[key])),
|
|
793
|
+
h('div', { key: label + '-hi', style: cheapNow ? undefined : activeCell, className: cheapNow ? 'dcm-cell-idle' : '' }, rate(high[key])),
|
|
606
794
|
]
|
|
607
795
|
|
|
608
|
-
// Breakdown row for the single-model case: tokens in the bucket and what
|
|
609
|
-
// they actually accrued, slot by slot, at the rate of each slot.
|
|
610
796
|
const soleSlots = perModel.length === 1 ? used[0].slots : null
|
|
611
797
|
const spentRow = (label, bucketField, rateField) => {
|
|
612
798
|
const n = Number(usage && usage[bucketField]) || 0
|
|
@@ -620,9 +806,9 @@ window.__ModuleLoader__.load({
|
|
|
620
806
|
usd = (n / 1e6) * (active[rateField] || 0)
|
|
621
807
|
}
|
|
622
808
|
return [
|
|
623
|
-
h('div', { key: label + '-l',
|
|
624
|
-
h('div', { key: label + '-n',
|
|
625
|
-
h('div', { key: label + '-v',
|
|
809
|
+
h('div', { key: label + '-l', className: 'dcm-td' }, label),
|
|
810
|
+
h('div', { key: label + '-n', className: 'dcm-num' }, tokens(n)),
|
|
811
|
+
h('div', { key: label + '-v', className: 'dcm-cost' }, money(usd * fx, currency)),
|
|
626
812
|
]
|
|
627
813
|
}
|
|
628
814
|
|
|
@@ -637,7 +823,8 @@ window.__ModuleLoader__.load({
|
|
|
637
823
|
return h('div', { ref: box, style: { position: 'relative' } }, [
|
|
638
824
|
h('button', {
|
|
639
825
|
key: 'chip', type: 'button',
|
|
640
|
-
|
|
826
|
+
className: 'dcm-chip',
|
|
827
|
+
style: { background: bg, color: fg, borderColor: cheapNow ? 'rgba(16,185,129,0.3)' : 'rgba(245,158,11,0.3)' },
|
|
641
828
|
onClick: () => setOpen((v) => !v),
|
|
642
829
|
title: flat ? 'Единый тариф' : cheapNow ? 'Льготный тариф' : 'Полный тариф',
|
|
643
830
|
}, [
|
|
@@ -645,12 +832,12 @@ window.__ModuleLoader__.load({
|
|
|
645
832
|
left ? h('span', { key: 'left', style: { opacity: 0.75 } }, left) : null,
|
|
646
833
|
]),
|
|
647
834
|
|
|
648
|
-
open ? h('div', { key: 'panel',
|
|
649
|
-
h('div', { key: 'title',
|
|
835
|
+
open ? h('div', { key: 'panel', className: 'dcm-panel' }, [
|
|
836
|
+
h('div', { key: 'title', className: 'dcm-panel-title' },
|
|
650
837
|
(state.route ? state.route.model : 'модель') +
|
|
651
838
|
(flat ? '' : ' — сейчас ' + (cheapNow ? 'не-пик' : t('peak')))),
|
|
652
839
|
|
|
653
|
-
h('div', { key: 'note',
|
|
840
|
+
h('div', { key: 'note', className: 'dcm-panel-note' },
|
|
654
841
|
flat
|
|
655
842
|
? 'Единая ставка, от времени суток не зависит'
|
|
656
843
|
: (cheapNow ? 'Действует льготная ставка' : 'Действует полная ставка') +
|
|
@@ -658,40 +845,40 @@ window.__ModuleLoader__.load({
|
|
|
658
845
|
|
|
659
846
|
flat
|
|
660
847
|
? null
|
|
661
|
-
: h('div', { key: 'clock',
|
|
848
|
+
: h('div', { key: 'clock', className: 'dcm-panel-note' },
|
|
662
849
|
'Пик: ' + windowText + ' ' + zl + ' · сейчас ' + nowLocal + ' ' + zl),
|
|
663
850
|
|
|
664
|
-
h('
|
|
851
|
+
h('hr', { key: 'hr1', className: 'dcm-hr' }),
|
|
665
852
|
|
|
666
|
-
h('div', { key: 'rates',
|
|
667
|
-
h('div', { key: 'h0',
|
|
668
|
-
flat ? null : h('div', { key: 'h1',
|
|
669
|
-
h('div', { key: 'h2',
|
|
853
|
+
h('div', { key: 'rates', className: flat ? 'dcm-panel-grid-flat' : 'dcm-panel-grid' }, [
|
|
854
|
+
h('div', { key: 'h0', className: 'dcm-th' }, '1M токенов, $'),
|
|
855
|
+
flat ? null : h('div', { key: 'h1', className: 'dcm-th-num' }, 'не-пик'),
|
|
856
|
+
h('div', { key: 'h2', className: 'dcm-th-num' }, flat ? 'ставка' : 'пик'),
|
|
670
857
|
rateRow('Вход (кэш-хит)', 'cacheHit'),
|
|
671
858
|
rateRow('Вход (мимо кэша)', 'input'),
|
|
672
859
|
showCacheWrite ? rateRow('Вход (запись в кэш)', 'cacheWrite') : null,
|
|
673
860
|
rateRow('Выход', 'output'),
|
|
674
861
|
]),
|
|
675
862
|
|
|
676
|
-
h('
|
|
863
|
+
h('hr', { key: 'hr2', className: 'dcm-hr' }),
|
|
677
864
|
|
|
678
865
|
multiModel
|
|
679
866
|
? null
|
|
680
867
|
: [
|
|
681
|
-
h('div', { key: 'btitle',
|
|
682
|
-
h('div', { key: 'breakdown',
|
|
683
|
-
h('div', { key: 'b0',
|
|
684
|
-
h('div', { key: 'b1',
|
|
685
|
-
h('div', { key: 'b2',
|
|
868
|
+
h('div', { key: 'btitle', className: 'dcm-section-lbl' }, 'Токены сессии'),
|
|
869
|
+
h('div', { key: 'breakdown', className: 'dcm-panel-grid-3' }, [
|
|
870
|
+
h('div', { key: 'b0', className: 'dcm-th' }, 'токенов'),
|
|
871
|
+
h('div', { key: 'b1', className: 'dcm-th-num' }, ''),
|
|
872
|
+
h('div', { key: 'b2', className: 'dcm-th-num' }, currency),
|
|
686
873
|
spentRow('Вход (кэш-хит)', 'cacheReadTokens', 'cacheHit'),
|
|
687
874
|
spentRow('Вход (мимо кэша)', 'uncachedInputTokens', 'input'),
|
|
688
875
|
showCacheWrite ? spentRow('Вход (запись в кэш)', 'cacheWriteTokens', 'cacheWrite') : null,
|
|
689
876
|
spentRow('Выход', 'outputTokens', 'output'),
|
|
690
877
|
]),
|
|
691
|
-
h('
|
|
878
|
+
h('hr', { key: 'hr-models', className: 'dcm-hr' }),
|
|
692
879
|
],
|
|
693
880
|
|
|
694
|
-
h('div', { key: 'mtitle',
|
|
881
|
+
h('div', { key: 'mtitle', className: 'dcm-section-lbl' }, 'Расход по моделям'),
|
|
695
882
|
h(
|
|
696
883
|
'div',
|
|
697
884
|
{ key: 'models-list', style: { display: 'flex', flexDirection: 'column', gap: 4 } },
|
|
@@ -728,7 +915,7 @@ window.__ModuleLoader__.load({
|
|
|
728
915
|
textAlign: 'right',
|
|
729
916
|
fontVariantNumeric: 'tabular-nums',
|
|
730
917
|
whiteSpace: 'nowrap',
|
|
731
|
-
color: m.known ?
|
|
918
|
+
color: m.known ? 'var(--dsw-alias-label-primary)' : 'var(--dsw-alias-label-tertiary)',
|
|
732
919
|
},
|
|
733
920
|
},
|
|
734
921
|
m.known ? (hasUsage ? money(m.cost, currency) : '—') : 'не указана стоимость',
|
|
@@ -738,14 +925,14 @@ window.__ModuleLoader__.load({
|
|
|
738
925
|
),
|
|
739
926
|
),
|
|
740
927
|
|
|
741
|
-
h('
|
|
928
|
+
h('hr', { key: 'hr3', className: 'dcm-hr' }),
|
|
742
929
|
|
|
743
|
-
h('div', { key: 'total',
|
|
930
|
+
h('div', { key: 'total', className: 'dcm-total' }, [
|
|
744
931
|
h('span', { key: 'l' }, 'Сессия:'),
|
|
745
932
|
h('span', { key: 'v' }, hasUsage ? '≈ ' + money(total, currency) : 'нет данных'),
|
|
746
933
|
]),
|
|
747
934
|
|
|
748
|
-
hasUsage ? null : h('div', { key: 'nodata',
|
|
935
|
+
hasUsage ? null : h('div', { key: 'nodata', className: 'dcm-panel-note', style: { paddingTop: 6 } },
|
|
749
936
|
'Провайдер ещё не сообщил usage для этой сессии.'),
|
|
750
937
|
]) : null,
|
|
751
938
|
])
|
|
@@ -756,22 +943,26 @@ window.__ModuleLoader__.load({
|
|
|
756
943
|
try { ctx.locale.register(NS, { en, ru }) } catch (_) {}
|
|
757
944
|
}
|
|
758
945
|
|
|
759
|
-
ctx
|
|
760
|
-
ctx.slots.register(
|
|
761
|
-
{ name: 'conversation.session.header.utilities', id: 'dsh-cost-meter', order: 10, inject: () => ({ ctx }) },
|
|
762
|
-
(p) => React.createElement(CostMeter, { ...p, ctx }),
|
|
763
|
-
),
|
|
764
|
-
)
|
|
946
|
+
refreshMirrorUntilVisible(ctx)
|
|
765
947
|
|
|
766
|
-
ctx.slots.inject
|
|
767
|
-
ctx.slots.
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
948
|
+
if (ctx.slots && typeof ctx.slots.inject === 'function') {
|
|
949
|
+
ctx.slots.inject('conversation.session.header.utilities', () =>
|
|
950
|
+
ctx.slots.register(
|
|
951
|
+
{ name: 'conversation.session.header.utilities', id: 'dsh-cost-meter', order: 10, inject: () => ({ ctx }) },
|
|
952
|
+
(p) => React.createElement(ErrorBoundary, null, React.createElement(CostMeter, { ...p, ctx: (p && p.ctx) || ctx })),
|
|
953
|
+
),
|
|
954
|
+
)
|
|
955
|
+
|
|
956
|
+
ctx.slots.inject('settings.plugin.item', () =>
|
|
957
|
+
ctx.slots.register(
|
|
958
|
+
{ name: 'settings.plugin.item', key: NS, locale: NS, order: 20, inject: () => ({ ctx }) },
|
|
959
|
+
(p) => React.createElement(ErrorBoundary, null, React.createElement(CostMeterCard, { ...p, ctx: (p && p.ctx) || ctx })),
|
|
960
|
+
),
|
|
961
|
+
)
|
|
962
|
+
}
|
|
772
963
|
}
|
|
773
964
|
|
|
774
|
-
module.exports = { apply, inject: ['slots', 'locale', 'settingsScope'] }
|
|
965
|
+
module.exports = { apply, inject: ['slots', 'locale', 'settingsScope'], CostMeterCard, CostMeter, ErrorBoundary, ensureStyles }
|
|
775
966
|
return module.exports
|
|
776
967
|
},
|
|
777
968
|
})
|