kingkont 0.14.6 → 0.15.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kingkont",
3
- "version": "0.14.6",
3
+ "version": "0.15.0",
4
4
  "description": "KingKont · Chatium — нод-редактор сцен с AI-генерацией (картинки/видео/голос/SFX/музыка/текст)",
5
5
  "main": "main.js",
6
6
  "bin": {
package/renderer/board.js CHANGED
@@ -587,6 +587,28 @@ async function renderWelcomeRecents() {
587
587
  }
588
588
  }
589
589
 
590
+ // Бейдж «N в фоне» в верхнем-левом углу обложки welcome-карточки.
591
+ // Показывается когда в этом проекте есть активные bg-jobs (см. bgJobsAll).
592
+ function _makeBgBadge(n) {
593
+ const b = document.createElement('div');
594
+ b.className = 'welcome-card-bg-badge';
595
+ b.title = `${n} ${_ru_jobs(n)} в фоне`;
596
+ b.textContent = `⏳ ${n}`;
597
+ return b;
598
+ }
599
+ function _ru_jobs(n) {
600
+ const n10 = n % 10, n100 = n % 100;
601
+ if (n100 >= 11 && n100 <= 14) return 'генераций';
602
+ if (n10 === 1) return 'генерация';
603
+ if (n10 >= 2 && n10 <= 4) return 'генерации';
604
+ return 'генераций';
605
+ }
606
+ // Live-update welcome когда bg-jobs меняются (start/end). Перерисовываем
607
+ // только если открыт welcome (нет проекта), иначе UI не виден.
608
+ window.addEventListener('bgjobs:changed', () => {
609
+ if (!state.filmHandle) renderWelcomeRecents().catch(() => {});
610
+ });
611
+
590
612
  // Карточка локального (папочного) проекта — извлечена из renderWelcomeRecents.
591
613
  // Удаление и save-as-template — через ПКМ (по аналогии с cloud-карточкой).
592
614
  function makeRecentWelcomeCard(r) {
@@ -600,6 +622,10 @@ function makeRecentWelcomeCard(r) {
600
622
  img.onload = () => setTimeout(() => URL.revokeObjectURL(img.src), 60_000);
601
623
  thumb.appendChild(img);
602
624
  } else thumb.textContent = '🎬';
625
+ // Бейдж «N в фоне» если в этом проекте крутятся генерации.
626
+ // Map проектов → projectKey: для folder-проектов = 'folder:' + r.name.
627
+ const bgList = (typeof bgJobsAll === 'function' ? bgJobsAll() : {})['folder:' + r.name];
628
+ if (bgList?.length) thumb.appendChild(_makeBgBadge(bgList.length));
603
629
  const meta = document.createElement('div');
604
630
  meta.className = 'welcome-card-meta';
605
631
  const nameEl = document.createElement('div');
@@ -753,6 +779,9 @@ function makeCloudWelcomeCard(p) {
753
779
  cloudBadge.textContent = '☁';
754
780
  cloudBadge.title = 'Облачный проект';
755
781
  thumb.appendChild(cloudBadge);
782
+ // Бейдж «N в фоне» если в этом проекте крутятся генерации.
783
+ const bgList = (typeof bgJobsAll === 'function' ? bgJobsAll() : {})['cloud:' + p.id];
784
+ if (bgList?.length) thumb.appendChild(_makeBgBadge(bgList.length));
756
785
 
757
786
  const meta = document.createElement('div');
758
787
  meta.className = 'welcome-card-meta';
package/renderer/chat.js CHANGED
@@ -99,9 +99,9 @@
99
99
  },
100
100
 
101
101
  add_node: {
102
- description: 'Добавить ноду на текущую доску. Для image/video/audio с prompt нода будет draft (юзер запустит generation отдельно или используй generate_node). Для audio: subKind="music"|"sfx"|"voice" (default: voice/TTS). aspectRatio: "16:9"|"9:16"|"1:1"|"3:4"|"4:3"|"21:9" — переопределяет дефолтный для текущей сцены. ВАЖНО: если юзер сказал "горизонтальная"16:9, "вертикальная"9:16, "квадрат" 1:1.',
103
- params: '{"type":"image|video|audio|text","subKind":"music|sfx|voice (only for audio)","name":"<optional>","prompt":"<optional>","x":<optional>,"y":<optional>,"text":"<for-type=text>","modelKey":"<optional>","durationMs":<for-music-or-sfx>,"aspectRatio":"<optional, for image/video>"}',
104
- async handler({ type, subKind, name, prompt, x, y, text, modelKey, durationMs, aspectRatio }) {
102
+ description: 'Добавить ноду на текущую доску. Типы: image, video, audio, text, label.\n - image/video/audio с prompt draft, юзер потом запустит generation (или используй generate_node).\n - audio: subKind="music"|"sfx"|"voice" (дефолт voice/TTS).\n - aspectRatio: "16:9"|"9:16"|"1:1"|"3:4"|"4:3"|"21:9" — переопределяет дефолт сцены. Если юзер сказал "горизонтальная"16:9, "вертикальная"9:16, "квадрат"→1:1.\n - text: длинный markdown-блок (несколько абзацев). Передай text="...".\n - label: КОРОТКАЯ подпись поверх холста (заголовок, метка). Передай text="..." (1-3 слова обычно). Опционально textStyle={fontSize, fontFamily, italic}.',
103
+ params: '{"type":"image|video|audio|text|label","subKind":"music|sfx|voice","name":"<optional>","prompt":"<for image/video/audio>","x":<optional>,"y":<optional>,"text":"<for text/label>","textStyle":{"fontSize":<12|14|18|24|32|48|64|96>,"italic":<bool>,"fontFamily":"default|pencil|handwritten|brush|marker|display|elegant|rounded|serif|mono"},"modelKey":"<optional>","durationMs":<for music/sfx>,"aspectRatio":"<optional>"}',
104
+ async handler({ type, subKind, name, prompt, x, y, text, textStyle, modelKey, durationMs, aspectRatio }) {
105
105
  if (!state.currentBoard) throw new Error('доска не выбрана');
106
106
  if (!['image','video','audio','text','label'].includes(type)) throw new Error(`unknown type: ${type}`);
107
107
  // Поиск незанятого места: пытаемся справа от последней ноды,
@@ -149,6 +149,14 @@
149
149
  node.text = text;
150
150
  // .md создастся при saveBoardMetadata.
151
151
  }
152
+ if (type === 'label') {
153
+ node.text = (typeof text === 'string' && text.trim()) ? text : 'Подпись';
154
+ // textStyle: дефолт совпадает с UI-добавленной подписью.
155
+ // Юзерские поля переопределяют по одному (не replace целиком).
156
+ const defaultStyle = { fontSize: 32, italic: false, fontFamily: 'pencil' };
157
+ node.textStyle = { ...defaultStyle, ...(textStyle || {}) };
158
+ // Label — auto-size по тексту, width/height не задаём.
159
+ }
152
160
  if (prompt && type !== 'text' && type !== 'label') {
153
161
  node.status = 'draft';
154
162
  node.generated = { rawPrompt: prompt, prompt, modelKey: modelKey || undefined };
@@ -1366,9 +1374,8 @@
1366
1374
  const panel = $('chatPanel');
1367
1375
  panel.classList.toggle('hidden');
1368
1376
  if (!panel.classList.contains('hidden')) {
1369
- // Сворачиваем preview-панель иначе она перекрывает чат справа
1370
- // (preview z-index 40 > chat z-index 30 by design).
1371
- if (typeof setPreviewCollapsed === 'function') setPreviewCollapsed(true);
1377
+ // Z-index сам разруливает: preview.collapsed ниже чата (z=40 vs 45),
1378
+ // preview открыт поверх (z=50). Auto-collapse больше не нужен.
1372
1379
  // Отрисовываем сохранённую историю при показе (на случай если она
1373
1380
  // была подгружена в фоне через loadFromCurrentProject до первого toggle).
1374
1381
  renderHistory();
@@ -1728,6 +1728,8 @@ async function startGenerationJob(node, kind, prompt, mediaRefs, boardHandle, bK
1728
1728
  const job = { boardKey: bKey, boardHandle, kind, taskId: null, nodeId: node.id };
1729
1729
  state.jobs.set(node.id, job);
1730
1730
  updateJobsBadge();
1731
+ // Track в global bg-jobs (для welcome-индикаторов и system-notif при завершении).
1732
+ if (typeof bgJobStart === 'function') bgJobStart({ nodeId: node.id, kind, name: node.name });
1731
1733
  logJob(node.id, `gen start kind=${kind} model=${modelKey || '—'} refs=${mediaRefs?.length || 0} prompt="${(prompt||'').slice(0,200)}"`);
1732
1734
  // Подробный дамп всех refs со всеми полями
1733
1735
  logJob(node.id, `refs dump: ${logSafe((mediaRefs || []).map(r => ({
@@ -1856,6 +1858,11 @@ async function startGenerationJob(node, kind, prompt, mediaRefs, boardHandle, bK
1856
1858
  });
1857
1859
  state.jobs.delete(node.id);
1858
1860
  updateJobsBadge();
1861
+ if (typeof bgJobEnd === 'function') bgJobEnd(node.id);
1862
+ if (typeof showToast === 'function') showToast(`⚠ ${kind || 'gen'}: ${e.message?.slice(0, 80)}`, 'error');
1863
+ if (typeof systemNotify === 'function' && document.hidden) {
1864
+ systemNotify('KingKont: ошибка генерации', e.message?.slice(0, 100), { tag: 'gen-err-' + node.id }).catch(() => {});
1865
+ }
1859
1866
  }
1860
1867
  }
1861
1868
 
@@ -1981,10 +1988,23 @@ async function pollJob(job, nodeId, bKey, boardHandle, kind) {
1981
1988
  });
1982
1989
  state.jobs.delete(nodeId);
1983
1990
  updateJobsBadge();
1991
+ // bgJob убираем (welcome перечитает счётчики через event).
1992
+ if (typeof bgJobEnd === 'function') bgJobEnd(nodeId);
1984
1993
  if (typeof window.refreshBalance === 'function') window.refreshBalance();
1985
- // Toast если завершилось НЕ на текущей доске (юзер мог переключиться).
1986
- if (typeof showToast === 'function' && state.currentBoard?.key !== bKey) {
1987
- showToast(`✓ ${kind} «${nodeName || nodeId.slice(0,6)}» готов в ${bKey}`, 'ok');
1994
+ // Toast ВСЕГДА (юзер хочет видеть когда что-то сделалось).
1995
+ // Стиль 'ok' для current-board, 'info' для другой доски.
1996
+ const isCurrent = state.currentBoard?.key === bKey;
1997
+ if (typeof showToast === 'function') {
1998
+ const where = isCurrent ? '' : ` в ${bKey}`;
1999
+ showToast(`✓ ${kind} «${nodeName || nodeId.slice(0,6)}» готов${where}`, isCurrent ? 'ok' : 'info');
2000
+ }
2001
+ // System notification — только когда генерация завершилась в фоне
2002
+ // (не на активной доске или когда окно скрыто). На активном UI
2003
+ // юзер и так видит результат — не нужен лишний попап.
2004
+ if (typeof systemNotify === 'function' && (!isCurrent || document.hidden)) {
2005
+ systemNotify(`KingKont: ${kind} готов`, `«${nodeName || ''}» в ${bKey}`, {
2006
+ tag: 'gen-' + nodeId,
2007
+ }).catch(() => {});
1988
2008
  }
1989
2009
  return;
1990
2010
  }
package/renderer/state.js CHANGED
@@ -70,6 +70,85 @@ async function listCharacters(filmHandle) {
70
70
  } catch { return []; }
71
71
  }
72
72
 
73
+ // === BG-JOBS TRACKING ===
74
+ // Хранит активные генерации МЕЖДУ всеми проектами в localStorage:
75
+ // bgJobs:<projectKey> = [{nodeId, kind, name, startedAt}]
76
+ // Используется для показа индикаторов на welcome-карточках («N генераций
77
+ // в фоне») и системных уведомлений когда что-то завершилось.
78
+ function bgJobsKey(projectKey) { return 'bgJobs:' + projectKey; }
79
+ function _projectKeyForCurrent() {
80
+ if (state.cloudProjectId) return 'cloud:' + state.cloudProjectId;
81
+ if (state.filmHandle?.name) return 'folder:' + state.filmHandle.name;
82
+ return null;
83
+ }
84
+ function bgJobStart(info) {
85
+ // info: {nodeId, kind, name?, projectKey?}
86
+ const pk = info.projectKey || _projectKeyForCurrent();
87
+ if (!pk) return;
88
+ try {
89
+ const list = JSON.parse(localStorage.getItem(bgJobsKey(pk)) || '[]');
90
+ if (list.some(j => j.nodeId === info.nodeId)) return; // уже трекается
91
+ list.push({ nodeId: info.nodeId, kind: info.kind, name: info.name || null, startedAt: Date.now() });
92
+ localStorage.setItem(bgJobsKey(pk), JSON.stringify(list));
93
+ window.dispatchEvent(new CustomEvent('bgjobs:changed'));
94
+ } catch {}
95
+ }
96
+ function bgJobEnd(nodeId, projectKey) {
97
+ const pk = projectKey || _projectKeyForCurrent();
98
+ if (!pk) return null;
99
+ try {
100
+ const list = JSON.parse(localStorage.getItem(bgJobsKey(pk)) || '[]');
101
+ const job = list.find(j => j.nodeId === nodeId);
102
+ const filtered = list.filter(j => j.nodeId !== nodeId);
103
+ if (filtered.length) localStorage.setItem(bgJobsKey(pk), JSON.stringify(filtered));
104
+ else localStorage.removeItem(bgJobsKey(pk));
105
+ window.dispatchEvent(new CustomEvent('bgjobs:changed'));
106
+ return job;
107
+ } catch { return null; }
108
+ }
109
+ function bgJobsAll() {
110
+ const result = {};
111
+ try {
112
+ for (let i = 0; i < localStorage.length; i++) {
113
+ const k = localStorage.key(i);
114
+ if (!k || !k.startsWith('bgJobs:')) continue;
115
+ const list = JSON.parse(localStorage.getItem(k) || '[]');
116
+ if (list.length) result[k.slice('bgJobs:'.length)] = list;
117
+ }
118
+ } catch {}
119
+ return result;
120
+ }
121
+ window.bgJobStart = bgJobStart;
122
+ window.bgJobEnd = bgJobEnd;
123
+ window.bgJobsAll = bgJobsAll;
124
+
125
+ // === SYSTEM NOTIFICATION (HTML5 Notification API) ===
126
+ // Просим разрешение лениво — на первом успешном вызове.
127
+ let _notifPermAsked = false;
128
+ async function systemNotify(title, body, opts = {}) {
129
+ if (typeof Notification === 'undefined') return null;
130
+ let perm = Notification.permission;
131
+ if (perm === 'default' && !_notifPermAsked) {
132
+ _notifPermAsked = true;
133
+ try { perm = await Notification.requestPermission(); } catch {}
134
+ }
135
+ if (perm !== 'granted') return null;
136
+ try {
137
+ const n = new Notification(title, {
138
+ body: body || '',
139
+ icon: opts.icon || 'assets/icon.png',
140
+ tag: opts.tag, // dedup по tag (одинаковый = заменяет)
141
+ silent: opts.silent === true,
142
+ });
143
+ if (opts.onClick) n.onclick = opts.onClick;
144
+ return n;
145
+ } catch (e) {
146
+ console.warn('systemNotify failed:', e?.message);
147
+ return null;
148
+ }
149
+ }
150
+ window.systemNotify = systemNotify;
151
+
73
152
  // Глобальный toast для информативных уведомлений (генерация завершилась,
74
153
  // resume сработал, ...). Стек справа сверху, auto-dismiss через 5s.
75
154
  function showToast(text, kind) {
@@ -253,10 +253,10 @@
253
253
  position: fixed; right: 0; top: 0; bottom: 0; width: 420px;
254
254
  background: #1a1a1a; border-left: 1px solid #333;
255
255
  display: flex; flex-direction: column;
256
- /* z-index 30НИЖЕ preview-panel (z-index 40). Preview перекрывает чат
257
- визуально; при открытии чата preview сворачивается автоматически
258
- (см. toggle() в chat.js → setPreviewCollapsed(true)). */
259
- z-index: 30;
256
+ /* z-index 45ВЫШЕ свёрнутого preview (40). Когда preview открыт
257
+ (НЕ .collapsed), он получает z-index 50 → перекрывает чат. См.
258
+ правило body:has(.preview-panel:not(.collapsed)) ниже. */
259
+ z-index: 45;
260
260
  box-shadow: -8px 0 32px rgba(0,0,0,0.4);
261
261
  /* --chat-font задаётся inline в chat.js (Cmd+/Cmd-). Дефолт 13px. */
262
262
  font-size: var(--chat-font, 13px);
@@ -265,6 +265,12 @@
265
265
  position спасал, но без этого max-width дочерних не работал. */
266
266
  overflow-x: hidden; box-sizing: border-box;
267
267
  }
268
+ /* Preview открыт (развёрнут) — поднимаем над чатом. Когда collapsed
269
+ (полоска справа) — преview опускается ниже чата (z-index по умолчанию
270
+ 40 vs наш chat 45). */
271
+ body:has(.preview-panel:not(.collapsed)) .preview-panel {
272
+ z-index: 50;
273
+ }
268
274
  .chat-panel.chat-drag::before {
269
275
  content: '📎 Отпусти, чтобы прикрепить файл к чату';
270
276
  position: absolute; inset: 0; z-index: 100;
@@ -490,6 +496,21 @@
490
496
  font-size: 13px; line-height: 1;
491
497
  backdrop-filter: blur(2px);
492
498
  }
499
+ /* «⏳ N» — индикатор активных фоновых генераций. Top-left угла обложки. */
500
+ .welcome-card-bg-badge {
501
+ position: absolute; left: 6px; top: 6px;
502
+ background: rgba(50, 30, 20, 0.88); color: #fc9;
503
+ border: 1px solid rgba(255, 200, 160, 0.4);
504
+ border-radius: 12px; padding: 2px 8px;
505
+ display: inline-flex; align-items: center; gap: 4px;
506
+ font-size: 11px; line-height: 1.2; font-weight: 500;
507
+ backdrop-filter: blur(2px);
508
+ animation: bgBadgePulse 1.6s ease-in-out infinite;
509
+ }
510
+ @keyframes bgBadgePulse {
511
+ 0%, 100% { opacity: 0.85; }
512
+ 50% { opacity: 1; }
513
+ }
493
514
  .welcome-card-meta { padding: 8px 12px; }
494
515
  .welcome-card-name { font-size: 13px; color: #ddd; word-break: break-all; }
495
516
  .welcome-card-ts { font-size: 11px; color: #666; margin-top: 2px; }