kingkont 0.9.0 → 0.9.2
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/index.html +7 -0
- package/lib/providers.js +12 -0
- package/package.json +1 -1
- package/renderer/board.js +75 -18
- package/renderer/settings.js +32 -24
- package/renderer/styles.css +34 -0
package/index.html
CHANGED
|
@@ -70,6 +70,13 @@
|
|
|
70
70
|
|
|
71
71
|
<!-- Welcome-экран: виден только когда body.no-project -->
|
|
72
72
|
<div class="welcome" id="welcome">
|
|
73
|
+
<!-- Топ-правый угол: identity + балансы. Позиционирование absolute,
|
|
74
|
+
чтобы не ехало с центрированной шапкой. Заполняется renderWelcomeStatus
|
|
75
|
+
(board.js) при каждом показе welcome И при auth-changed/balance-tick. -->
|
|
76
|
+
<div class="welcome-status" id="welcomeStatus" style="-webkit-app-region: no-drag;">
|
|
77
|
+
<div class="welcome-status-balances" id="welcomeStatusBalances"></div>
|
|
78
|
+
<div class="welcome-status-identity" id="welcomeStatusIdentity"></div>
|
|
79
|
+
</div>
|
|
73
80
|
<div class="welcome-inner">
|
|
74
81
|
<div class="welcome-header">
|
|
75
82
|
<img class="welcome-logo" src="assets/icon.png" alt="" draggable="false" id="welcomeLogo" title="Дабл-клик — настройки" style="cursor:pointer;">
|
package/lib/providers.js
CHANGED
|
@@ -712,6 +712,18 @@ async function fetchBalances(s) {
|
|
|
712
712
|
}
|
|
713
713
|
} catch {}
|
|
714
714
|
}
|
|
715
|
+
if (s.useKie && process.env.KIE_API_KEY) {
|
|
716
|
+
try {
|
|
717
|
+
// KIE: GET /api/v1/chat/credit → { code: 200, msg: 'success', data: <number> }
|
|
718
|
+
const r = await fetch(`${KIE_BASE}/api/v1/chat/credit`, {
|
|
719
|
+
headers: { 'Authorization': `Bearer ${process.env.KIE_API_KEY}` },
|
|
720
|
+
});
|
|
721
|
+
const d = await r.json().catch(() => ({}));
|
|
722
|
+
if (r.ok && d.code === 200 && typeof d.data === 'number') {
|
|
723
|
+
out.kie = { unit: 'credits', amount: d.data };
|
|
724
|
+
}
|
|
725
|
+
} catch {}
|
|
726
|
+
}
|
|
715
727
|
return out;
|
|
716
728
|
}
|
|
717
729
|
|
package/package.json
CHANGED
package/renderer/board.js
CHANGED
|
@@ -50,8 +50,11 @@ window.addEventListener('DOMContentLoaded', async () => {
|
|
|
50
50
|
// Сбрасываем кэш — он содержал данные старого юзера.
|
|
51
51
|
localStorage.removeItem('cloudProjectsCache');
|
|
52
52
|
localStorage.removeItem('cloudProjectsLastOpened');
|
|
53
|
-
//
|
|
54
|
-
//
|
|
53
|
+
// Welcome top-right identity-pill + balances — перерисовываем сразу
|
|
54
|
+
// (даже если открыт проект и welcome скрыт — обновится для следующего показа).
|
|
55
|
+
renderWelcomeIdentity().catch(() => {});
|
|
56
|
+
refreshBalance().catch(() => {});
|
|
57
|
+
// Если открыт welcome (нет проекта) — перерисовать grid.
|
|
55
58
|
if (!state || !window.cloudProjects) return;
|
|
56
59
|
if (window.cloudProjects.setVisibility) window.cloudProjects.setVisibility();
|
|
57
60
|
if (!document.body.classList.contains('no-project')) return;
|
|
@@ -162,39 +165,91 @@ window.addEventListener('DOMContentLoaded', async () => {
|
|
|
162
165
|
// red (≤0). Также экспортирована глобально как window.refreshBalance —
|
|
163
166
|
// чтобы settings-окно могло триггерить обновление после login/logout.
|
|
164
167
|
async function refreshBalance() {
|
|
165
|
-
const wrap = document.getElementById('balancesAll');
|
|
166
|
-
if (!wrap) return;
|
|
167
168
|
let data = {};
|
|
168
169
|
try {
|
|
169
170
|
const r = await fetch('/api/balance/all');
|
|
170
171
|
if (r.ok) data = await r.json();
|
|
171
172
|
} catch {}
|
|
172
|
-
wrap.innerHTML = '';
|
|
173
173
|
// Один pill на провайдер. Если у провайдера нет данных (выключен или
|
|
174
174
|
// API не дал баланс) — pill не рендерим.
|
|
175
175
|
const pills = [
|
|
176
176
|
{ key: 'kingkont', label: 'KingKont', onClick: () => window.openTxLog?.(), low: 100, fmt: (a) => `<b>${a.toLocaleString('ru-RU')}</b> credits` },
|
|
177
|
+
{ key: 'kie', label: 'KIE', low: 5, fmt: (a) => `<b>${a.toLocaleString('ru-RU', { maximumFractionDigits: 2 })}</b> credits` },
|
|
177
178
|
{ key: 'openrouter', label: 'OpenRouter', low: 0.5, fmt: (a) => `<b>$${a.toFixed(2)}</b>` },
|
|
178
179
|
{ key: 'elevenlabs', label: 'ElevenLabs', low: 1000, fmt: (a) => `<b>${a.toLocaleString('ru-RU')}</b> chars` },
|
|
179
180
|
];
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
pill.
|
|
191
|
-
pill.
|
|
181
|
+
function renderInto(wrap) {
|
|
182
|
+
if (!wrap) return;
|
|
183
|
+
wrap.innerHTML = '';
|
|
184
|
+
for (const p of pills) {
|
|
185
|
+
const d = data[p.key];
|
|
186
|
+
if (!d || typeof d.amount !== 'number') continue;
|
|
187
|
+
const pill = document.createElement('span');
|
|
188
|
+
pill.className = 'balance-info';
|
|
189
|
+
pill.title = `Баланс ${p.label}` + (p.onClick ? ' · клик — лог списаний' : '');
|
|
190
|
+
if (d.amount > 0 && d.amount < (p.low || 0)) pill.classList.add('low');
|
|
191
|
+
if (d.amount <= 0) pill.classList.add('empty');
|
|
192
|
+
pill.innerHTML = `<span class="dot"></span><span style="color:#888;font-size:10px;margin-right:4px;">${p.label}</span><span>${p.fmt(d.amount)}</span>`;
|
|
193
|
+
if (p.onClick) {
|
|
194
|
+
pill.style.cursor = 'pointer';
|
|
195
|
+
pill.addEventListener('click', p.onClick);
|
|
196
|
+
}
|
|
197
|
+
wrap.appendChild(pill);
|
|
192
198
|
}
|
|
193
|
-
wrap.appendChild(pill);
|
|
194
199
|
}
|
|
200
|
+
// Sidebar-footer (когда открыт проект).
|
|
201
|
+
renderInto(document.getElementById('balancesAll'));
|
|
202
|
+
// Welcome top-right (когда no-project).
|
|
203
|
+
renderInto(document.getElementById('welcomeStatusBalances'));
|
|
195
204
|
}
|
|
196
205
|
window.refreshBalance = refreshBalance;
|
|
197
206
|
|
|
207
|
+
// Identity-pill в правом верхнем углу welcome-экрана.
|
|
208
|
+
// Показывает кто залогинен в KingKont (или предлагает войти).
|
|
209
|
+
async function renderWelcomeIdentity() {
|
|
210
|
+
const wrap = document.getElementById('welcomeStatusIdentity');
|
|
211
|
+
if (!wrap) return;
|
|
212
|
+
wrap.innerHTML = '';
|
|
213
|
+
let status = null;
|
|
214
|
+
try { status = await window.appChatium?.status?.(); } catch {}
|
|
215
|
+
if (status?.connected) {
|
|
216
|
+
// Имя: предпочитаем display-name (login/name из auth~me), fallback — userId.
|
|
217
|
+
const name = status.login || status.name || status.userId || 'KingKont';
|
|
218
|
+
const sub = status.userId && status.userId !== name ? `· ${status.userId.slice(0, 8)}` : '';
|
|
219
|
+
wrap.innerHTML = `
|
|
220
|
+
<span style="color:#5c5; font-size:13px; line-height:1;">●</span>
|
|
221
|
+
<span class="who">${escapeHtml(name)}</span>
|
|
222
|
+
<span class="who-sub">${escapeHtml(sub)}</span>
|
|
223
|
+
`;
|
|
224
|
+
const logoutBtn = document.createElement('button');
|
|
225
|
+
logoutBtn.textContent = 'Выйти';
|
|
226
|
+
logoutBtn.title = 'Logout из KingKont';
|
|
227
|
+
logoutBtn.addEventListener('click', async () => {
|
|
228
|
+
if (!confirm('Выйти из KingKont?')) return;
|
|
229
|
+
try { await window.appChatium?.logout?.(); } catch {}
|
|
230
|
+
});
|
|
231
|
+
wrap.appendChild(logoutBtn);
|
|
232
|
+
} else {
|
|
233
|
+
wrap.innerHTML = `
|
|
234
|
+
<span style="color:#888; font-size:13px; line-height:1;">●</span>
|
|
235
|
+
<span class="who" style="color:#aaa;">Не залогинен</span>
|
|
236
|
+
`;
|
|
237
|
+
const loginBtn = document.createElement('button');
|
|
238
|
+
loginBtn.textContent = 'Войти';
|
|
239
|
+
loginBtn.title = 'Войти в KingKont';
|
|
240
|
+
loginBtn.addEventListener('click', async () => {
|
|
241
|
+
try { await window.appChatium?.login?.(); } catch (e) { alert('Login failed: ' + (e?.message || e)); }
|
|
242
|
+
});
|
|
243
|
+
wrap.appendChild(loginBtn);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
window.renderWelcomeIdentity = renderWelcomeIdentity;
|
|
247
|
+
function escapeHtml(s) {
|
|
248
|
+
return String(s || '').replace(/[&<>"']/g, c => ({
|
|
249
|
+
'&':'&','<':'<','>':'>','"':'"',"'":''',
|
|
250
|
+
}[c]));
|
|
251
|
+
}
|
|
252
|
+
|
|
198
253
|
// === Лог списаний (модал с историей кредитов) ===
|
|
199
254
|
async function openTxLog() {
|
|
200
255
|
const modal = document.getElementById('txLogModal');
|
|
@@ -396,6 +451,8 @@ async function renderWelcomeRecents() {
|
|
|
396
451
|
if (!grid || !wrap) return;
|
|
397
452
|
grid.innerHTML = '';
|
|
398
453
|
wrap.style.display = '';
|
|
454
|
+
// Identity-pill в правом верхнем — обновляем при каждом показе welcome.
|
|
455
|
+
renderWelcomeIdentity().catch(() => {});
|
|
399
456
|
const list = await getRecents();
|
|
400
457
|
|
|
401
458
|
// Первой картой — «Открыть проект». Кликается → дёргает скрытый
|
package/renderer/settings.js
CHANGED
|
@@ -239,25 +239,33 @@ async function renderNodeBody(node, body) {
|
|
|
239
239
|
if (node.status === 'draft') {
|
|
240
240
|
const wrap = document.createElement('div');
|
|
241
241
|
wrap.className = 'gen-pending';
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
: node.type === 'image' ? '🖼' : '📝';
|
|
247
|
-
const st = document.createElement('div');
|
|
248
|
-
st.className = 'state-text';
|
|
249
|
-
st.textContent = 'Черновик: подведи входы и запусти';
|
|
250
|
-
const pp = document.createElement('div');
|
|
251
|
-
pp.className = 'prompt-preview';
|
|
252
|
-
pp.textContent = node.generated?.rawPrompt || node.generated?.prompt || '';
|
|
253
|
-
wrap.append(ic, st, pp);
|
|
242
|
+
// Для image — кнопка наверху, без декоративных 🖼/«Черновик» (юзер
|
|
243
|
+
// и так видит ноду пустой). Для audio/video оставляем — у них
|
|
244
|
+
// меньше визуального контекста и иконка помогает идентифицировать тип.
|
|
245
|
+
const isImage = node.type === 'image';
|
|
254
246
|
const runBtn = document.createElement('button');
|
|
255
247
|
runBtn.textContent = '▶ Запустить генерацию';
|
|
256
248
|
runBtn.className = 'primary';
|
|
257
|
-
runBtn.style.cssText =
|
|
249
|
+
runBtn.style.cssText = isImage
|
|
250
|
+
? 'font-size:12px; padding:6px 10px; align-self:stretch;'
|
|
251
|
+
: 'margin-top:8px; font-size:12px; padding:6px 10px;';
|
|
258
252
|
runBtn.addEventListener('mousedown', e => e.stopPropagation());
|
|
259
253
|
runBtn.addEventListener('click', e => { e.stopPropagation(); regenerateNode(node); });
|
|
260
|
-
|
|
254
|
+
const pp = document.createElement('div');
|
|
255
|
+
pp.className = 'prompt-preview';
|
|
256
|
+
pp.textContent = node.generated?.rawPrompt || node.generated?.prompt || '';
|
|
257
|
+
if (isImage) {
|
|
258
|
+
wrap.append(runBtn, pp);
|
|
259
|
+
} else {
|
|
260
|
+
const ic = document.createElement('div');
|
|
261
|
+
ic.style.cssText = 'font-size:36px; opacity:0.7;';
|
|
262
|
+
ic.textContent = node.type === 'audio' ? '🎙'
|
|
263
|
+
: node.type === 'video' ? '🎬' : '📝';
|
|
264
|
+
const st = document.createElement('div');
|
|
265
|
+
st.className = 'state-text';
|
|
266
|
+
st.textContent = 'Черновик: подведи входы и запусти';
|
|
267
|
+
wrap.append(ic, st, pp, runBtn);
|
|
268
|
+
}
|
|
261
269
|
body.appendChild(wrap);
|
|
262
270
|
return;
|
|
263
271
|
}
|
|
@@ -520,21 +528,21 @@ async function renderNodeBody(node, body) {
|
|
|
520
528
|
body.appendChild(playRow);
|
|
521
529
|
}
|
|
522
530
|
} else if (node.type === 'image' && !node.file && (node.generated?.rawPrompt || node.generated?.prompt)) {
|
|
523
|
-
//
|
|
524
|
-
//
|
|
525
|
-
//
|
|
531
|
+
// Image-нода с промптом, но без файла (ещё не генерилась).
|
|
532
|
+
// Кнопка ▶ Запустить — сверху (без декоративной 🖼-иконки и
|
|
533
|
+
// подписи «Не сгенерировано» — они только занимают место).
|
|
526
534
|
const wrap = document.createElement('div');
|
|
527
535
|
wrap.className = 'gen-pending';
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
536
|
+
const runBtn = document.createElement('button');
|
|
537
|
+
runBtn.textContent = '▶ Запустить генерацию';
|
|
538
|
+
runBtn.className = 'primary';
|
|
539
|
+
runBtn.style.cssText = 'font-size:12px; padding:6px 10px; align-self:stretch;';
|
|
540
|
+
runBtn.addEventListener('mousedown', e => e.stopPropagation());
|
|
541
|
+
runBtn.addEventListener('click', e => { e.stopPropagation(); regenerateNode(node); });
|
|
534
542
|
const pp = document.createElement('div');
|
|
535
543
|
pp.className = 'prompt-preview';
|
|
536
544
|
pp.textContent = node.generated?.rawPrompt || node.generated?.prompt || '';
|
|
537
|
-
wrap.append(
|
|
545
|
+
wrap.append(runBtn, pp);
|
|
538
546
|
body.appendChild(wrap);
|
|
539
547
|
}
|
|
540
548
|
}
|
package/renderer/styles.css
CHANGED
|
@@ -213,6 +213,40 @@
|
|
|
213
213
|
vertical-align: middle; margin-left: 6px;
|
|
214
214
|
}
|
|
215
215
|
.welcome-sub { font-size: 12px; color: #888; letter-spacing: 0.5px; text-transform: uppercase; }
|
|
216
|
+
/* Топ-правый блок welcome-экрана: identity + balances. Положение fixed,
|
|
217
|
+
чтобы не зависеть от центрированной .welcome-inner колонки. */
|
|
218
|
+
.welcome-status {
|
|
219
|
+
position: fixed; top: 16px; right: 24px; z-index: 60;
|
|
220
|
+
display: flex; flex-direction: column; align-items: flex-end; gap: 6px;
|
|
221
|
+
pointer-events: none; /* контейнер прозрачен, дочерние сами включают pointer-events */
|
|
222
|
+
}
|
|
223
|
+
.welcome-status-identity {
|
|
224
|
+
pointer-events: auto;
|
|
225
|
+
display: flex; align-items: center; gap: 8px;
|
|
226
|
+
background: rgba(30, 32, 40, 0.7); border: 1px solid #333;
|
|
227
|
+
border-radius: 999px; padding: 4px 10px;
|
|
228
|
+
font-size: 12px; color: #ccc;
|
|
229
|
+
backdrop-filter: blur(6px);
|
|
230
|
+
}
|
|
231
|
+
.welcome-status-identity .who {
|
|
232
|
+
color: #e0e0e0; font-weight: 500; max-width: 240px;
|
|
233
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
234
|
+
}
|
|
235
|
+
.welcome-status-identity .who-sub {
|
|
236
|
+
color: #888; font-size: 10px;
|
|
237
|
+
font-family: ui-monospace, 'SF Mono', monospace;
|
|
238
|
+
}
|
|
239
|
+
.welcome-status-identity button {
|
|
240
|
+
background: transparent; border: none; color: #9ab;
|
|
241
|
+
font-size: 11px; cursor: pointer; padding: 2px 6px; border-radius: 4px;
|
|
242
|
+
}
|
|
243
|
+
.welcome-status-identity button:hover { background: rgba(255,255,255,0.06); color: #cde; }
|
|
244
|
+
.welcome-status-balances {
|
|
245
|
+
pointer-events: auto;
|
|
246
|
+
display: flex; flex-direction: row; gap: 6px; flex-wrap: wrap;
|
|
247
|
+
justify-content: flex-end;
|
|
248
|
+
}
|
|
249
|
+
/* Используем те же .balance-info pill'ы что и в sidebar-footer'е. */
|
|
216
250
|
.welcome-open {
|
|
217
251
|
margin-top: 16px;
|
|
218
252
|
padding: 12px 28px; font-size: 15px; font-weight: 600;
|