@goodandready/dsh-cost-meter 0.7.9 → 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/docs/design/DESIGN.md +6 -2
- package/docs/plans/findings.md +11 -20
- package/docs/plans/progress.md +24 -16
- package/docs/plans/task_plan.md +23 -30
- package/lib/client.js +480 -338
- 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,133 +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
|
-
'field.useOpenRouter': 'Use OpenRouter Catalog',
|
|
53
|
-
'hint.useOpenRouter': 'Pull rates and discount windows automatically from the OpenRouter catalog.',
|
|
54
|
-
'field.refreshHours': 'Catalog Refresh Interval (hours)',
|
|
55
|
-
'hint.refreshHours': 'How often to fetch fresh model rates from OpenRouter (default: 24).',
|
|
56
|
-
}
|
|
16
|
+
// ----------------------------------------------------------- localization
|
|
57
17
|
|
|
58
18
|
const ru = {
|
|
59
|
-
title: '
|
|
60
|
-
subtitle: '
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
'
|
|
75
|
-
'
|
|
76
|
-
'
|
|
77
|
-
'
|
|
78
|
-
'settings.saved': 'Сохранено',
|
|
79
|
-
'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': 'Источник тарифа',
|
|
80
38
|
'field.currency': 'Символ валюты',
|
|
39
|
+
'hint.currency': 'Символ валюты для отображения расходов (например, $, ₽, €).',
|
|
81
40
|
'field.usdRate': 'Курс валюты к USD',
|
|
82
|
-
'
|
|
83
|
-
'
|
|
84
|
-
'hint.
|
|
85
|
-
'
|
|
86
|
-
'
|
|
87
|
-
'
|
|
88
|
-
'
|
|
89
|
-
'
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (ChevronIcon) {
|
|
102
|
-
return React.createElement(ChevronIcon, {
|
|
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
|
-
})
|
|
109
|
-
}
|
|
110
|
-
return React.createElement('svg', {
|
|
111
|
-
className: 'dcm-chev' + (props.open ? ' dcm-chev-open' : ''),
|
|
112
|
-
style: {
|
|
113
|
-
marginLeft: 'auto', flex: 'none', color: 'var(--dsw-alias-label-tertiary)',
|
|
114
|
-
transition: 'transform .16s', transform: props.open ? 'rotate(180deg)' : 'none',
|
|
115
|
-
},
|
|
116
|
-
width: 14, height: 14, viewBox: '0 0 14 14', fill: 'none', 'aria-hidden': 'true',
|
|
117
|
-
}, React.createElement('path', {
|
|
118
|
-
d: 'M3.5 5.25L7 8.75L10.5 5.25', stroke: 'currentColor', strokeWidth: 1.5,
|
|
119
|
-
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
120
|
-
}))
|
|
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: 'не-пик',
|
|
121
60
|
}
|
|
122
61
|
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
'
|
|
126
|
-
'.
|
|
127
|
-
'.
|
|
128
|
-
'.
|
|
129
|
-
'.
|
|
130
|
-
'.
|
|
131
|
-
'.
|
|
132
|
-
'.
|
|
133
|
-
'.
|
|
134
|
-
'.
|
|
135
|
-
'.
|
|
136
|
-
'.
|
|
137
|
-
'.
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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',
|
|
147
104
|
}
|
|
148
105
|
|
|
149
106
|
function makeT(dict, fallback) {
|
|
@@ -167,6 +124,189 @@ window.__ModuleLoader__.load({
|
|
|
167
124
|
)
|
|
168
125
|
}
|
|
169
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
|
+
|
|
170
310
|
function CostMeterCard(props) {
|
|
171
311
|
const ctx = props.ctx
|
|
172
312
|
const locale = useActiveLocale(ctx)
|
|
@@ -176,6 +316,7 @@ window.__ModuleLoader__.load({
|
|
|
176
316
|
const [saving, setSaving] = React.useState(false)
|
|
177
317
|
const [err, setErr] = React.useState('')
|
|
178
318
|
const [saved, setSaved] = React.useState(false)
|
|
319
|
+
const [diagState, setDiagState] = React.useState(null)
|
|
179
320
|
|
|
180
321
|
const settingsScope = (ctx && typeof ctx.get === 'function' ? ctx.get('settingsScope') : null) || (ctx && ctx.settingsScope) || (ctx && ctx.services && ctx.services.settingsScope)
|
|
181
322
|
const scope = React.useMemo(
|
|
@@ -189,7 +330,16 @@ window.__ModuleLoader__.load({
|
|
|
189
330
|
React.useCallback(() => ({ status: 'loading' }), []),
|
|
190
331
|
)
|
|
191
332
|
|
|
192
|
-
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])
|
|
193
343
|
|
|
194
344
|
const status = (snapshot && snapshot.status) || 'loading'
|
|
195
345
|
const stored = (snapshot && snapshot.value) || {}
|
|
@@ -206,10 +356,15 @@ window.__ModuleLoader__.load({
|
|
|
206
356
|
}
|
|
207
357
|
}, [status, stored, draft])
|
|
208
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
|
+
|
|
209
363
|
const save = async () => {
|
|
210
364
|
if (!scope || !draft) return
|
|
211
365
|
setSaving(true); setErr(''); setSaved(false)
|
|
212
366
|
const broken = []
|
|
367
|
+
|
|
213
368
|
try {
|
|
214
369
|
await scope.set('currency', String(draft.currency || '$'))
|
|
215
370
|
} catch (e) { broken.push('currency: ' + (e && e.message || String(e))) }
|
|
@@ -237,8 +392,8 @@ window.__ModuleLoader__.load({
|
|
|
237
392
|
} catch (e) { broken.push('useOpenRouter: ' + (e && e.message || String(e))) }
|
|
238
393
|
|
|
239
394
|
const hoursNum = Number(String(draft.refreshHours).trim())
|
|
240
|
-
if (!Number.isFinite(hoursNum) || hoursNum
|
|
241
|
-
broken.push('refreshHours: must be
|
|
395
|
+
if (!Number.isFinite(hoursNum) || hoursNum < 1 || hoursNum > 168) {
|
|
396
|
+
broken.push('refreshHours: must be between 1 and 168 hours')
|
|
242
397
|
} else {
|
|
243
398
|
try {
|
|
244
399
|
await scope.set('refreshHours', hoursNum)
|
|
@@ -254,81 +409,152 @@ window.__ModuleLoader__.load({
|
|
|
254
409
|
setTimeout(() => setSaved(false), 2500)
|
|
255
410
|
}
|
|
256
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
|
+
|
|
257
418
|
return React.createElement('li', { className: 'dcm-card' },
|
|
258
419
|
React.createElement('button', {
|
|
259
420
|
type: 'button', className: 'dcm-head', 'aria-expanded': open,
|
|
260
421
|
onClick: () => setOpen((v) => !v),
|
|
261
422
|
},
|
|
262
|
-
React.createElement('
|
|
263
|
-
React.createElement('span', { className: 'dcm-title' }, t('title')),
|
|
264
|
-
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
|
+
),
|
|
265
439
|
),
|
|
266
440
|
React.createElement(Chevron, { open }),
|
|
267
441
|
),
|
|
268
442
|
open ? React.createElement('div', { className: 'dcm-body' },
|
|
269
443
|
status === 'loading'
|
|
270
|
-
? React.createElement('
|
|
444
|
+
? React.createElement('div', { className: 'dcm-hint', style: { padding: '12px 0' } }, t('settings.loading'))
|
|
271
445
|
: status !== 'ready'
|
|
272
|
-
? React.createElement('
|
|
273
|
-
: React.createElement(
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
React.createElement('
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
+
),
|
|
293
487
|
),
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
className: 'dcm-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}
|
|
311
|
-
|
|
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
|
+
),
|
|
312
516
|
),
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
React.createElement('
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
+
),
|
|
324
548
|
),
|
|
325
|
-
|
|
326
|
-
|
|
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,
|
|
327
553
|
React.createElement('div', { className: 'dcm-foot' },
|
|
328
554
|
React.createElement('button', {
|
|
329
555
|
type: 'button',
|
|
330
|
-
className: 'dcm-
|
|
331
|
-
disabled: saving,
|
|
556
|
+
className: 'dcm-btn dcm-btn-primary',
|
|
557
|
+
disabled: saving || !tzValid || !rateValid || !hoursValid,
|
|
332
558
|
onClick: save,
|
|
333
559
|
}, saving ? t('settings.saving') : t('settings.save')),
|
|
334
560
|
),
|
|
@@ -337,21 +563,18 @@ window.__ModuleLoader__.load({
|
|
|
337
563
|
)
|
|
338
564
|
}
|
|
339
565
|
|
|
340
|
-
|
|
341
566
|
// ---------------------------------------------------------------- tariff
|
|
342
567
|
|
|
343
568
|
function inWindow(minute, w) {
|
|
344
569
|
return w.start <= w.end ? minute >= w.start && minute < w.end : minute >= w.start || minute < w.end
|
|
345
570
|
}
|
|
346
571
|
|
|
347
|
-
/** Rates in force at a given UTC minute: the last matching window, else base. */
|
|
348
572
|
function ratesAt(schedule, minute) {
|
|
349
573
|
const windows = (schedule && schedule.windows) || []
|
|
350
574
|
for (const w of windows) if (inWindow(minute, w)) return w.rates
|
|
351
575
|
return (schedule && schedule.base) || { cacheHit: 0, input: 0, cacheWrite: 0, output: 0 }
|
|
352
576
|
}
|
|
353
577
|
|
|
354
|
-
/** Minutes until the rate card changes, or null when it never does. */
|
|
355
578
|
function minutesUntilChange(schedule, minute) {
|
|
356
579
|
const now = ratesAt(schedule, minute)
|
|
357
580
|
for (let step = 1; step <= DAY; step += 1) {
|
|
@@ -361,10 +584,6 @@ window.__ModuleLoader__.load({
|
|
|
361
584
|
return null
|
|
362
585
|
}
|
|
363
586
|
|
|
364
|
-
/**
|
|
365
|
-
* The day's two extremes, by output rate: what the widget labels
|
|
366
|
-
* "не-пик" and "пик". Equal extremes mean a flat tariff.
|
|
367
|
-
*/
|
|
368
587
|
function extremes(schedule) {
|
|
369
588
|
let low = null
|
|
370
589
|
let high = null
|
|
@@ -376,7 +595,6 @@ window.__ModuleLoader__.load({
|
|
|
376
595
|
return { low: low || {}, high: high || {}, flat: !low || !high || low.output === high.output }
|
|
377
596
|
}
|
|
378
597
|
|
|
379
|
-
/** Total tokens across every half-hour slot of one model. */
|
|
380
598
|
function sumSlots(slots) {
|
|
381
599
|
const total = { uncachedInputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 }
|
|
382
600
|
for (const key of Object.keys(slots || {})) {
|
|
@@ -386,11 +604,6 @@ window.__ModuleLoader__.load({
|
|
|
386
604
|
return total
|
|
387
605
|
}
|
|
388
606
|
|
|
389
|
-
/**
|
|
390
|
-
* Cost of one model, each half-hour slot charged at the rate that applied
|
|
391
|
-
* in that slot. Slot 14 is 07:00–07:30 UTC, so its midpoint (07:15) picks
|
|
392
|
-
* the window without landing on a boundary.
|
|
393
|
-
*/
|
|
394
607
|
function spendBySlots(slots, schedule) {
|
|
395
608
|
let usd = 0
|
|
396
609
|
for (const key of Object.keys(slots || {})) {
|
|
@@ -400,7 +613,6 @@ window.__ModuleLoader__.load({
|
|
|
400
613
|
return usd
|
|
401
614
|
}
|
|
402
615
|
|
|
403
|
-
/** Session spend in USD for one rate card. */
|
|
404
616
|
function spendUsd(usage, rates) {
|
|
405
617
|
const u = usage || {}
|
|
406
618
|
const perMillion =
|
|
@@ -411,16 +623,15 @@ window.__ModuleLoader__.load({
|
|
|
411
623
|
return perMillion / 1e6
|
|
412
624
|
}
|
|
413
625
|
|
|
414
|
-
// --------------------------------------------------------------- format
|
|
415
|
-
|
|
416
626
|
function rate(value) {
|
|
417
627
|
if (!value) return '—'
|
|
418
628
|
const text = value < 0.1 ? value.toFixed(3) : value.toFixed(2)
|
|
419
|
-
return '$' + text.replace(/(
|
|
629
|
+
return '$' + text.replace(/(\\.\\d*?)0+$/, '$1').replace(/\\.$/, '')
|
|
420
630
|
}
|
|
421
631
|
|
|
422
632
|
function money(value, currency) {
|
|
423
|
-
|
|
633
|
+
const n = Number(value) || 0
|
|
634
|
+
return currency + n.toFixed(Math.abs(n) >= 100 ? 0 : 2)
|
|
424
635
|
}
|
|
425
636
|
|
|
426
637
|
function tokens(value) {
|
|
@@ -439,20 +650,6 @@ window.__ModuleLoader__.load({
|
|
|
439
650
|
return String(Math.floor(minute / 60)).padStart(2, '0') + ':' + String(minute % 60).padStart(2, '0')
|
|
440
651
|
}
|
|
441
652
|
|
|
442
|
-
function isValidTimeZone(tz) {
|
|
443
|
-
if (!tz || typeof tz !== 'string') return false
|
|
444
|
-
try {
|
|
445
|
-
new Intl.DateTimeFormat(undefined, { timeZone: tz })
|
|
446
|
-
return true
|
|
447
|
-
} catch {
|
|
448
|
-
return false
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
/**
|
|
453
|
-
* Render a UTC minute-of-day in the display zone. Built from a real Date so
|
|
454
|
-
* the zone's current offset (and any DST) comes from Intl, not arithmetic.
|
|
455
|
-
*/
|
|
456
653
|
function hhmmInZone(minute, zone, reference) {
|
|
457
654
|
const d = new Date(Date.UTC(
|
|
458
655
|
reference.getUTCFullYear(), reference.getUTCMonth(), reference.getUTCDate(), 0, minute,
|
|
@@ -466,7 +663,6 @@ window.__ModuleLoader__.load({
|
|
|
466
663
|
}
|
|
467
664
|
}
|
|
468
665
|
|
|
469
|
-
/** Short zone label for the panel: "МСК" for Moscow, else the city part. */
|
|
470
666
|
function zoneLabel(zone) {
|
|
471
667
|
if (!zone) return 'UTC'
|
|
472
668
|
if (zone === 'Europe/Moscow') return 'МСК'
|
|
@@ -474,56 +670,7 @@ window.__ModuleLoader__.load({
|
|
|
474
670
|
return city.replace(/_/g, ' ')
|
|
475
671
|
}
|
|
476
672
|
|
|
477
|
-
|
|
478
|
-
if (!timestamp) return 'нет данных'
|
|
479
|
-
const minutes = Math.max(0, Math.round((Date.now() - timestamp) / 60000))
|
|
480
|
-
if (minutes < 60) return minutes + ' мин назад'
|
|
481
|
-
const hours = Math.round(minutes / 60)
|
|
482
|
-
return hours < 48 ? hours + ' ч назад' : Math.round(hours / 24) + ' дн назад'
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
// ---------------------------------------------------------------- styles
|
|
486
|
-
|
|
487
|
-
const C = {
|
|
488
|
-
label: 'var(--dsw-alias-label-primary)',
|
|
489
|
-
muted: 'var(--dsw-alias-label-tertiary)',
|
|
490
|
-
line: 'var(--dsw-alias-border-l2)',
|
|
491
|
-
panelBg: 'var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-base))',
|
|
492
|
-
okFg: 'var(--dsw-alias-state-success-primary)',
|
|
493
|
-
okBg: 'var(--dsw-alias-state-success-tertiary)',
|
|
494
|
-
warnFg: 'var(--dsw-alias-state-warn-primary)',
|
|
495
|
-
warnBg: 'var(--dsw-alias-state-warn-tertiary)',
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
const S = {
|
|
499
|
-
chip: {
|
|
500
|
-
display: 'inline-flex', alignItems: 'center', gap: 6, height: 22, padding: '0 8px',
|
|
501
|
-
borderRadius: 6, border: 'none', cursor: 'pointer', font: 'inherit', fontSize: 12,
|
|
502
|
-
fontVariantNumeric: 'tabular-nums', lineHeight: '22px',
|
|
503
|
-
},
|
|
504
|
-
panel: {
|
|
505
|
-
position: 'absolute', top: 28, right: 0, zIndex: 40, width: 320, padding: '12px 14px',
|
|
506
|
-
borderRadius: 10, border: '1px solid ' + C.line, background: C.panelBg,
|
|
507
|
-
boxShadow: '0 10px 30px rgba(0,0,0,.28)', fontSize: 12, lineHeight: 1.5,
|
|
508
|
-
color: C.label, textAlign: 'left', fontVariantNumeric: 'tabular-nums',
|
|
509
|
-
},
|
|
510
|
-
title: { fontWeight: 600, paddingBottom: 4 },
|
|
511
|
-
note: { color: C.muted },
|
|
512
|
-
hr: { height: 1, background: C.line, margin: '10px -14px' },
|
|
513
|
-
grid: { display: 'grid', gridTemplateColumns: '1fr 60px 60px', columnGap: 6, alignItems: 'center' },
|
|
514
|
-
gridFlat: { display: 'grid', gridTemplateColumns: '1fr 72px', columnGap: 6, alignItems: 'center' },
|
|
515
|
-
grid3: { display: 'grid', gridTemplateColumns: '1fr 58px 72px', columnGap: 6, alignItems: 'center' },
|
|
516
|
-
th: { color: C.muted, paddingBottom: 4 },
|
|
517
|
-
thNum: { color: C.muted, paddingBottom: 4, textAlign: 'right' },
|
|
518
|
-
td: { padding: '3px 0' },
|
|
519
|
-
cellIdle: { textAlign: 'right', padding: '3px 6px', borderRadius: 4, color: C.muted },
|
|
520
|
-
num: { textAlign: 'right', color: C.muted, padding: '3px 0' },
|
|
521
|
-
cost: { textAlign: 'right', padding: '3px 0' },
|
|
522
|
-
section: { color: C.muted, paddingBottom: 4 },
|
|
523
|
-
total: { display: 'flex', justifyContent: 'space-between', fontWeight: 600 },
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
// ------------------------------------------------------------ component
|
|
673
|
+
// ------------------------------------------------------------ CostMeter
|
|
527
674
|
|
|
528
675
|
function CostMeter(props) {
|
|
529
676
|
const ctx = props.ctx
|
|
@@ -536,6 +683,9 @@ window.__ModuleLoader__.load({
|
|
|
536
683
|
const [open, setOpen] = React.useState(false)
|
|
537
684
|
const [now, setNow] = React.useState(() => new Date())
|
|
538
685
|
const box = React.useRef(null)
|
|
686
|
+
|
|
687
|
+
React.useEffect(() => { ensureStyles() }, [])
|
|
688
|
+
|
|
539
689
|
const settingsScope = (ctx && typeof ctx.get === 'function' ? ctx.get('settingsScope') : null) || (ctx && ctx.settingsScope) || (ctx && ctx.services && ctx.services.settingsScope)
|
|
540
690
|
const settingsSnap = React.useSyncExternalStore
|
|
541
691
|
? React.useSyncExternalStore(
|
|
@@ -545,10 +695,6 @@ window.__ModuleLoader__.load({
|
|
|
545
695
|
)
|
|
546
696
|
: null
|
|
547
697
|
|
|
548
|
-
// Every model this session actually billed, so each part is priced on its
|
|
549
|
-
// own tariff instead of the current model's. Usage arrives split into
|
|
550
|
-
// half-hour slots of the UTC day — spend is charged at the rate that was
|
|
551
|
-
// in force then, so a tariff switch never reprices what is already spent.
|
|
552
698
|
const used = (byModel && Array.isArray(byModel.models) ? byModel.models : [])
|
|
553
699
|
.map((m) => ({ ...m, usage: sumSlots(m && m.slots) }))
|
|
554
700
|
.filter((m) => m.usage.uncachedInputTokens || m.usage.outputTokens ||
|
|
@@ -567,8 +713,7 @@ window.__ModuleLoader__.load({
|
|
|
567
713
|
})
|
|
568
714
|
.catch(() => {})
|
|
569
715
|
}, [])
|
|
570
|
-
|
|
571
|
-
// own tariff before its tokens can be priced.
|
|
716
|
+
|
|
572
717
|
React.useEffect(() => { load(usedKeys) }, [load, usedKeys])
|
|
573
718
|
React.useEffect(() => { if (open) load(usedKeys) }, [open, load, usedKeys])
|
|
574
719
|
React.useEffect(() => {
|
|
@@ -591,18 +736,18 @@ window.__ModuleLoader__.load({
|
|
|
591
736
|
const hasUsage = usage !== undefined && usage !== null
|
|
592
737
|
const minute = now.getUTCHours() * 60 + now.getUTCMinutes()
|
|
593
738
|
|
|
594
|
-
// No rate card for this route: say so instead of showing a fake zero.
|
|
595
739
|
if (!schedule) {
|
|
596
740
|
return h('div', { ref: box, style: { position: 'relative' } }, [
|
|
597
741
|
h('button', {
|
|
598
742
|
key: 'chip', type: 'button',
|
|
599
|
-
|
|
743
|
+
className: 'dcm-chip',
|
|
744
|
+
style: { background: 'transparent', color: 'var(--dsw-alias-label-tertiary)' },
|
|
600
745
|
onClick: () => setOpen((v) => !v), title: t('chipUnknown'),
|
|
601
746
|
}, 'тариф ?'),
|
|
602
|
-
open ? h('div', { key: 'panel',
|
|
603
|
-
h('div', { key: 't',
|
|
747
|
+
open ? h('div', { key: 'panel', className: 'dcm-panel' }, [
|
|
748
|
+
h('div', { key: 't', className: 'dcm-panel-title' },
|
|
604
749
|
state.route ? state.route.provider + ' / ' + state.route.model : t('routeUnknown')),
|
|
605
|
-
h('div', { key: 'n',
|
|
750
|
+
h('div', { key: 'n', className: 'dcm-panel-note' },
|
|
606
751
|
state.catalogError
|
|
607
752
|
? t('catalogUnavailable') + state.catalogError
|
|
608
753
|
: 'Модель не найдена в каталоге OpenRouter. Задайте ставки вручную в настройках (prices) или сопоставьте модель через modelMap.'),
|
|
@@ -615,9 +760,6 @@ window.__ModuleLoader__.load({
|
|
|
615
760
|
const cheapNow = !flat && active.output === low.output
|
|
616
761
|
const left = clock(minutesUntilChange(schedule, minute))
|
|
617
762
|
|
|
618
|
-
// Per-model spend: each model on its own tariff, at that tariff's rate
|
|
619
|
-
// for the current moment. Falls back to the session-wide totals when the
|
|
620
|
-
// per-model projection is unavailable (older harness, headless).
|
|
621
763
|
const schedules = state.schedules || {}
|
|
622
764
|
const perModel = used.map((m) => {
|
|
623
765
|
const key = m.provider + '/' + m.model
|
|
@@ -635,27 +777,22 @@ window.__ModuleLoader__.load({
|
|
|
635
777
|
const total = perModel.length
|
|
636
778
|
? perModel.reduce((sum, m) => sum + m.cost, 0)
|
|
637
779
|
: spendUsd(usage, active) * fx
|
|
638
|
-
const unpriced = perModel.filter((m) => !m.known)
|
|
639
780
|
|
|
640
|
-
const fg = cheapNow ?
|
|
641
|
-
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)'
|
|
642
783
|
const activeCell = { textAlign: 'right', padding: '3px 6px', borderRadius: 4, background: bg, color: fg, fontWeight: 600 }
|
|
643
784
|
|
|
644
|
-
// Two tiers: cheap and full side by side, the active one filled.
|
|
645
|
-
// One tier: a single plain column, no highlight to explain.
|
|
646
785
|
const rateRow = (label, key) => flat
|
|
647
786
|
? [
|
|
648
|
-
h('div', { key: label + '-l',
|
|
649
|
-
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])),
|
|
650
789
|
]
|
|
651
790
|
: [
|
|
652
|
-
h('div', { key: label + '-l',
|
|
653
|
-
h('div', { key: label + '-lo', style: cheapNow ? activeCell :
|
|
654
|
-
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])),
|
|
655
794
|
]
|
|
656
795
|
|
|
657
|
-
// Breakdown row for the single-model case: tokens in the bucket and what
|
|
658
|
-
// they actually accrued, slot by slot, at the rate of each slot.
|
|
659
796
|
const soleSlots = perModel.length === 1 ? used[0].slots : null
|
|
660
797
|
const spentRow = (label, bucketField, rateField) => {
|
|
661
798
|
const n = Number(usage && usage[bucketField]) || 0
|
|
@@ -669,9 +806,9 @@ window.__ModuleLoader__.load({
|
|
|
669
806
|
usd = (n / 1e6) * (active[rateField] || 0)
|
|
670
807
|
}
|
|
671
808
|
return [
|
|
672
|
-
h('div', { key: label + '-l',
|
|
673
|
-
h('div', { key: label + '-n',
|
|
674
|
-
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)),
|
|
675
812
|
]
|
|
676
813
|
}
|
|
677
814
|
|
|
@@ -686,7 +823,8 @@ window.__ModuleLoader__.load({
|
|
|
686
823
|
return h('div', { ref: box, style: { position: 'relative' } }, [
|
|
687
824
|
h('button', {
|
|
688
825
|
key: 'chip', type: 'button',
|
|
689
|
-
|
|
826
|
+
className: 'dcm-chip',
|
|
827
|
+
style: { background: bg, color: fg, borderColor: cheapNow ? 'rgba(16,185,129,0.3)' : 'rgba(245,158,11,0.3)' },
|
|
690
828
|
onClick: () => setOpen((v) => !v),
|
|
691
829
|
title: flat ? 'Единый тариф' : cheapNow ? 'Льготный тариф' : 'Полный тариф',
|
|
692
830
|
}, [
|
|
@@ -694,12 +832,12 @@ window.__ModuleLoader__.load({
|
|
|
694
832
|
left ? h('span', { key: 'left', style: { opacity: 0.75 } }, left) : null,
|
|
695
833
|
]),
|
|
696
834
|
|
|
697
|
-
open ? h('div', { key: 'panel',
|
|
698
|
-
h('div', { key: 'title',
|
|
835
|
+
open ? h('div', { key: 'panel', className: 'dcm-panel' }, [
|
|
836
|
+
h('div', { key: 'title', className: 'dcm-panel-title' },
|
|
699
837
|
(state.route ? state.route.model : 'модель') +
|
|
700
838
|
(flat ? '' : ' — сейчас ' + (cheapNow ? 'не-пик' : t('peak')))),
|
|
701
839
|
|
|
702
|
-
h('div', { key: 'note',
|
|
840
|
+
h('div', { key: 'note', className: 'dcm-panel-note' },
|
|
703
841
|
flat
|
|
704
842
|
? 'Единая ставка, от времени суток не зависит'
|
|
705
843
|
: (cheapNow ? 'Действует льготная ставка' : 'Действует полная ставка') +
|
|
@@ -707,40 +845,40 @@ window.__ModuleLoader__.load({
|
|
|
707
845
|
|
|
708
846
|
flat
|
|
709
847
|
? null
|
|
710
|
-
: h('div', { key: 'clock',
|
|
848
|
+
: h('div', { key: 'clock', className: 'dcm-panel-note' },
|
|
711
849
|
'Пик: ' + windowText + ' ' + zl + ' · сейчас ' + nowLocal + ' ' + zl),
|
|
712
850
|
|
|
713
|
-
h('
|
|
851
|
+
h('hr', { key: 'hr1', className: 'dcm-hr' }),
|
|
714
852
|
|
|
715
|
-
h('div', { key: 'rates',
|
|
716
|
-
h('div', { key: 'h0',
|
|
717
|
-
flat ? null : h('div', { key: 'h1',
|
|
718
|
-
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 ? 'ставка' : 'пик'),
|
|
719
857
|
rateRow('Вход (кэш-хит)', 'cacheHit'),
|
|
720
858
|
rateRow('Вход (мимо кэша)', 'input'),
|
|
721
859
|
showCacheWrite ? rateRow('Вход (запись в кэш)', 'cacheWrite') : null,
|
|
722
860
|
rateRow('Выход', 'output'),
|
|
723
861
|
]),
|
|
724
862
|
|
|
725
|
-
h('
|
|
863
|
+
h('hr', { key: 'hr2', className: 'dcm-hr' }),
|
|
726
864
|
|
|
727
865
|
multiModel
|
|
728
866
|
? null
|
|
729
867
|
: [
|
|
730
|
-
h('div', { key: 'btitle',
|
|
731
|
-
h('div', { key: 'breakdown',
|
|
732
|
-
h('div', { key: 'b0',
|
|
733
|
-
h('div', { key: 'b1',
|
|
734
|
-
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),
|
|
735
873
|
spentRow('Вход (кэш-хит)', 'cacheReadTokens', 'cacheHit'),
|
|
736
874
|
spentRow('Вход (мимо кэша)', 'uncachedInputTokens', 'input'),
|
|
737
875
|
showCacheWrite ? spentRow('Вход (запись в кэш)', 'cacheWriteTokens', 'cacheWrite') : null,
|
|
738
876
|
spentRow('Выход', 'outputTokens', 'output'),
|
|
739
877
|
]),
|
|
740
|
-
h('
|
|
878
|
+
h('hr', { key: 'hr-models', className: 'dcm-hr' }),
|
|
741
879
|
],
|
|
742
880
|
|
|
743
|
-
h('div', { key: 'mtitle',
|
|
881
|
+
h('div', { key: 'mtitle', className: 'dcm-section-lbl' }, 'Расход по моделям'),
|
|
744
882
|
h(
|
|
745
883
|
'div',
|
|
746
884
|
{ key: 'models-list', style: { display: 'flex', flexDirection: 'column', gap: 4 } },
|
|
@@ -777,7 +915,7 @@ window.__ModuleLoader__.load({
|
|
|
777
915
|
textAlign: 'right',
|
|
778
916
|
fontVariantNumeric: 'tabular-nums',
|
|
779
917
|
whiteSpace: 'nowrap',
|
|
780
|
-
color: m.known ?
|
|
918
|
+
color: m.known ? 'var(--dsw-alias-label-primary)' : 'var(--dsw-alias-label-tertiary)',
|
|
781
919
|
},
|
|
782
920
|
},
|
|
783
921
|
m.known ? (hasUsage ? money(m.cost, currency) : '—') : 'не указана стоимость',
|
|
@@ -787,14 +925,14 @@ window.__ModuleLoader__.load({
|
|
|
787
925
|
),
|
|
788
926
|
),
|
|
789
927
|
|
|
790
|
-
h('
|
|
928
|
+
h('hr', { key: 'hr3', className: 'dcm-hr' }),
|
|
791
929
|
|
|
792
|
-
h('div', { key: 'total',
|
|
930
|
+
h('div', { key: 'total', className: 'dcm-total' }, [
|
|
793
931
|
h('span', { key: 'l' }, 'Сессия:'),
|
|
794
932
|
h('span', { key: 'v' }, hasUsage ? '≈ ' + money(total, currency) : 'нет данных'),
|
|
795
933
|
]),
|
|
796
934
|
|
|
797
|
-
hasUsage ? null : h('div', { key: 'nodata',
|
|
935
|
+
hasUsage ? null : h('div', { key: 'nodata', className: 'dcm-panel-note', style: { paddingTop: 6 } },
|
|
798
936
|
'Провайдер ещё не сообщил usage для этой сессии.'),
|
|
799
937
|
]) : null,
|
|
800
938
|
])
|
|
@@ -805,22 +943,26 @@ window.__ModuleLoader__.load({
|
|
|
805
943
|
try { ctx.locale.register(NS, { en, ru }) } catch (_) {}
|
|
806
944
|
}
|
|
807
945
|
|
|
808
|
-
ctx
|
|
809
|
-
ctx.slots.register(
|
|
810
|
-
{ name: 'conversation.session.header.utilities', id: 'dsh-cost-meter', order: 10, inject: () => ({ ctx }) },
|
|
811
|
-
(p) => React.createElement(CostMeter, { ...p, ctx }),
|
|
812
|
-
),
|
|
813
|
-
)
|
|
946
|
+
refreshMirrorUntilVisible(ctx)
|
|
814
947
|
|
|
815
|
-
ctx.slots.inject
|
|
816
|
-
ctx.slots.
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
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
|
+
}
|
|
821
963
|
}
|
|
822
964
|
|
|
823
|
-
module.exports = { apply, inject: ['slots', 'locale', 'settingsScope'] }
|
|
965
|
+
module.exports = { apply, inject: ['slots', 'locale', 'settingsScope'], CostMeterCard, CostMeter, ErrorBoundary, ensureStyles }
|
|
824
966
|
return module.exports
|
|
825
967
|
},
|
|
826
968
|
})
|