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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kingkont",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "KingKont · Chatium — нод-редактор сцен с AI-генерацией (картинки/видео/голос/SFX/музыка/текст)",
5
5
  "main": "main.js",
6
6
  "bin": {
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
- // Если открыт welcome (нет проекта)перерисовать. fetchListCached
54
- // в renderWelcomeRecents подтянет свежий список с сервера.
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>&nbsp;credits` },
177
+ { key: 'kie', label: 'KIE', low: 5, fmt: (a) => `<b>${a.toLocaleString('ru-RU', { maximumFractionDigits: 2 })}</b>&nbsp;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>&nbsp;chars` },
179
180
  ];
180
- for (const p of pills) {
181
- const d = data[p.key];
182
- if (!d || typeof d.amount !== 'number') continue;
183
- const pill = document.createElement('span');
184
- pill.className = 'balance-info';
185
- pill.title = `Баланс ${p.label}` + (p.onClick ? ' · клик — лог списаний' : '');
186
- if (d.amount > 0 && d.amount < (p.low || 0)) pill.classList.add('low');
187
- if (d.amount <= 0) pill.classList.add('empty');
188
- 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>`;
189
- if (p.onClick) {
190
- pill.style.cursor = 'pointer';
191
- pill.addEventListener('click', p.onClick);
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
+ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;',
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
  // Первой картой — «Открыть проект». Кликается → дёргает скрытый
@@ -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
- const ic = document.createElement('div');
243
- ic.style.cssText = 'font-size:36px; opacity:0.7;';
244
- ic.textContent = node.type === 'audio' ? '🎙'
245
- : node.type === 'video' ? '🎬'
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 = 'margin-top:8px; font-size:12px; padding:6px 10px;';
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
- wrap.appendChild(runBtn);
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
- // Fallback: image-нода с промптом, но без файла (ещё не генерилась
524
- // и status != 'draft'). Показываем preview-блок чтобы юзер видел
525
- // что собирается генерироваться.
531
+ // Image-нода с промптом, но без файла (ещё не генерилась).
532
+ // Кнопка Запустить сверху (без декоративной 🖼-иконки и
533
+ // подписи «Не сгенерировано» — они только занимают место).
526
534
  const wrap = document.createElement('div');
527
535
  wrap.className = 'gen-pending';
528
- const ic = document.createElement('div');
529
- ic.style.cssText = 'font-size:36px; opacity:0.7;';
530
- ic.textContent = '🖼';
531
- const st = document.createElement('div');
532
- st.className = 'state-text';
533
- st.textContent = 'Не сгенерировано открой для запуска';
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(ic, st, pp);
545
+ wrap.append(runBtn, pp);
538
546
  body.appendChild(wrap);
539
547
  }
540
548
  }
@@ -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;