kingkont 0.14.7 → 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 +1 -1
- package/renderer/board.js +29 -0
- package/renderer/chat.js +2 -3
- package/renderer/generate.js +23 -3
- package/renderer/state.js +79 -0
- package/renderer/styles.css +25 -4
package/package.json
CHANGED
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
|
@@ -1374,9 +1374,8 @@
|
|
|
1374
1374
|
const panel = $('chatPanel');
|
|
1375
1375
|
panel.classList.toggle('hidden');
|
|
1376
1376
|
if (!panel.classList.contains('hidden')) {
|
|
1377
|
-
//
|
|
1378
|
-
//
|
|
1379
|
-
if (typeof setPreviewCollapsed === 'function') setPreviewCollapsed(true);
|
|
1377
|
+
// Z-index сам разруливает: preview.collapsed → ниже чата (z=40 vs 45),
|
|
1378
|
+
// preview открыт → поверх (z=50). Auto-collapse больше не нужен.
|
|
1380
1379
|
// Отрисовываем сохранённую историю при показе (на случай если она
|
|
1381
1380
|
// была подгружена в фоне через loadFromCurrentProject до первого toggle).
|
|
1382
1381
|
renderHistory();
|
package/renderer/generate.js
CHANGED
|
@@ -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
|
-
|
|
1987
|
-
|
|
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) {
|
package/renderer/styles.css
CHANGED
|
@@ -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
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
z-index:
|
|
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; }
|