kingkont 0.14.7 → 0.15.1
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/lib/providers.js +6 -1
- package/package.json +1 -1
- package/renderer/board.js +29 -0
- package/renderer/chat.js +70 -9
- package/renderer/generate.js +23 -3
- package/renderer/state.js +79 -0
- package/renderer/styles.css +25 -4
package/lib/providers.js
CHANGED
|
@@ -602,7 +602,12 @@ async function generateMusic(args) {
|
|
|
602
602
|
const { prompt, durationMs, settings: s } = args;
|
|
603
603
|
if (!prompt) throw new Error('нужен prompt');
|
|
604
604
|
|
|
605
|
-
|
|
605
|
+
// Приоритет direct ElevenLabs если включён + есть ключ — у юзера могут
|
|
606
|
+
// быть кастомные саб-настройки и платный ElevenLabs (зачем тратить
|
|
607
|
+
// Chatium-кредиты). Аналогичная логика в generateTts/generateSfx.
|
|
608
|
+
const directElevenAvailable = s.useElevenlabs && process.env.ELEVENLABS_API_KEY;
|
|
609
|
+
|
|
610
|
+
if (s.useChatium && s.chatium?.token && s.chatium?.base && !directElevenAvailable) {
|
|
606
611
|
return await audioViaChatium(s, { kind: 'music', prompt, durationMs });
|
|
607
612
|
}
|
|
608
613
|
if (!s.useElevenlabs) throw new Error('Войдите в KingKont или ElevenLabs для аудио.');
|
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
|
@@ -183,6 +183,50 @@
|
|
|
183
183
|
},
|
|
184
184
|
},
|
|
185
185
|
|
|
186
|
+
move_node: {
|
|
187
|
+
description: 'Передвинуть ноду в новые координаты (или сдвинуть относительно текущих через dx/dy). x/y абсолютные в canvas-coord. Если нужно сдвинуть несколько нод подряд — выдавай несколько move_node.',
|
|
188
|
+
params: '{"id":"<node-id>","x":<absolute>,"y":<absolute>,"dx":<relative-shift>,"dy":<relative-shift>}',
|
|
189
|
+
async handler({ id, x, y, dx, dy }) {
|
|
190
|
+
const b = state.currentBoard;
|
|
191
|
+
if (!b) throw new Error('доска не выбрана');
|
|
192
|
+
const node = b.metadata.nodes.find(n => n.id === id);
|
|
193
|
+
if (!node) throw new Error(`нода не найдена: ${id}`);
|
|
194
|
+
if (typeof x === 'number') node.x = x;
|
|
195
|
+
if (typeof y === 'number') node.y = y;
|
|
196
|
+
if (typeof dx === 'number') node.x = (node.x || 0) + dx;
|
|
197
|
+
if (typeof dy === 'number') node.y = (node.y || 0) + dy;
|
|
198
|
+
// Clamp к >= 50 чтобы не уехала в негатив за viewport.
|
|
199
|
+
if (node.x < 50) node.x = 50;
|
|
200
|
+
if (node.y < 50) node.y = 50;
|
|
201
|
+
scheduleSave();
|
|
202
|
+
if (typeof renderCanvas === 'function') await renderCanvas();
|
|
203
|
+
return { ok: true, x: node.x, y: node.y };
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
edit_scene_json: {
|
|
208
|
+
description: 'Прямое редактирование scene.json текущей доски — для нестандартных операций (массовая правка позиций, sort нод, чистка metadata, и т.п.). Передай patch как объект — поля заменяют существующие. Для замены ВСЕГО массива nodes — передай {nodes: [...]}. ВНИМАНИЕ: ошибка тут разрушит сцену — обязательно вызови take_snapshot ПЕРЕД.',
|
|
209
|
+
params: '{"patch":{...}}',
|
|
210
|
+
async handler({ patch }) {
|
|
211
|
+
const b = state.currentBoard;
|
|
212
|
+
if (!b) throw new Error('доска не выбрана');
|
|
213
|
+
if (!patch || typeof patch !== 'object') throw new Error('patch должен быть объектом');
|
|
214
|
+
// Применяем patch к metadata (top-level merge). Для nodes/connections
|
|
215
|
+
// если переданы — заменяем массив целиком (не merge).
|
|
216
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
217
|
+
b.metadata[k] = v;
|
|
218
|
+
}
|
|
219
|
+
scheduleSave();
|
|
220
|
+
if (typeof renderCanvas === 'function') await renderCanvas();
|
|
221
|
+
if (typeof renderConnections === 'function') renderConnections();
|
|
222
|
+
return {
|
|
223
|
+
ok: true,
|
|
224
|
+
nodesCount: (b.metadata.nodes || []).length,
|
|
225
|
+
connectionsCount: (b.metadata.connections || []).length,
|
|
226
|
+
};
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
|
|
186
230
|
update_node_prompt: {
|
|
187
231
|
description: 'Обновить промпт у image/video/audio ноды (без re-генерации).',
|
|
188
232
|
params: '{"id":"<node-id>","prompt":"<new>"}',
|
|
@@ -278,9 +322,18 @@
|
|
|
278
322
|
async handler({ nodeId, trackKind, start, duration }) {
|
|
279
323
|
const b = state.currentBoard;
|
|
280
324
|
if (!b) throw new Error('доска не выбрана');
|
|
325
|
+
// Re-read scene с диска чтобы видеть свежий node.file (после
|
|
326
|
+
// background-генерации). Без этого chat'оподобный flow «добавь и
|
|
327
|
+
// сразу в таймлайн» — файла ещё нет в node.file in-memory.
|
|
328
|
+
try {
|
|
329
|
+
const sceneFh = await b.handle.getFileHandle('scene.json');
|
|
330
|
+
const txt = await (await sceneFh.getFile()).text();
|
|
331
|
+
const scene = JSON.parse(txt);
|
|
332
|
+
if (Array.isArray(scene.nodes)) b.metadata.nodes = scene.nodes;
|
|
333
|
+
} catch {}
|
|
281
334
|
const node = b.metadata.nodes.find(n => n.id === nodeId);
|
|
282
335
|
if (!node) throw new Error(`нода не найдена: ${nodeId}`);
|
|
283
|
-
if (!node.file) throw new Error('у ноды нет файла (нужно сначала сгенерировать)');
|
|
336
|
+
if (!node.file) throw new Error('у ноды нет файла (нужно сначала сгенерировать и дождаться)');
|
|
284
337
|
if (typeof getTimeline !== 'function') throw new Error('getTimeline недоступен');
|
|
285
338
|
const tl = getTimeline();
|
|
286
339
|
const tk = trackKind === 'audio' ? 'audio' : 'video';
|
|
@@ -304,8 +357,14 @@
|
|
|
304
357
|
};
|
|
305
358
|
track.clips.push(clip);
|
|
306
359
|
scheduleSave();
|
|
307
|
-
|
|
308
|
-
|
|
360
|
+
// Force open + render: иначе thumbnails не загружаются (рендер скип'ался
|
|
361
|
+
// при скрытой панели, и при следующем открытии состояние UI было stale).
|
|
362
|
+
const tlPanel = document.getElementById('timelinePanel');
|
|
363
|
+
if (tlPanel?.classList.contains('hidden')) {
|
|
364
|
+
// Открываем таймлайн чтобы юзер видел что добавилось.
|
|
365
|
+
document.getElementById('timelineBtn')?.click();
|
|
366
|
+
} else if (typeof renderTimeline === 'function') {
|
|
367
|
+
await renderTimeline();
|
|
309
368
|
}
|
|
310
369
|
return { ok: true, clipId: clip.id, trackId: track.id, start: clip.start, duration: clip.duration };
|
|
311
370
|
},
|
|
@@ -1374,9 +1433,8 @@
|
|
|
1374
1433
|
const panel = $('chatPanel');
|
|
1375
1434
|
panel.classList.toggle('hidden');
|
|
1376
1435
|
if (!panel.classList.contains('hidden')) {
|
|
1377
|
-
//
|
|
1378
|
-
//
|
|
1379
|
-
if (typeof setPreviewCollapsed === 'function') setPreviewCollapsed(true);
|
|
1436
|
+
// Z-index сам разруливает: preview.collapsed → ниже чата (z=40 vs 45),
|
|
1437
|
+
// preview открыт → поверх (z=50). Auto-collapse больше не нужен.
|
|
1380
1438
|
// Отрисовываем сохранённую историю при показе (на случай если она
|
|
1381
1439
|
// была подгружена в фоне через loadFromCurrentProject до первого toggle).
|
|
1382
1440
|
renderHistory();
|
|
@@ -1424,12 +1482,15 @@
|
|
|
1424
1482
|
history = []; snapshots = [];
|
|
1425
1483
|
renderHistory(); renderSnapshotBar();
|
|
1426
1484
|
},
|
|
1427
|
-
// Reset-on-close: останавливаем polling
|
|
1428
|
-
//
|
|
1485
|
+
// Reset-on-close: НЕ останавливаем polling — серверный loop может ещё
|
|
1486
|
+
// ждать tool-results. Если бросим polling, loop зависает (tools никто
|
|
1487
|
+
// не исполнит). Клиентский UI чистим, но poll продолжается в фоне на
|
|
1488
|
+
// сессию ПОСЛЕДНЕГО открытого проекта (она же станет «фоновым chat'ом»).
|
|
1489
|
+
// При открытии другого проекта — startPolling переключит sessionKey.
|
|
1429
1490
|
resetInMemory: async () => {
|
|
1430
|
-
stopPolling();
|
|
1431
1491
|
history = []; snapshots = [];
|
|
1432
1492
|
renderHistory(); renderSnapshotBar();
|
|
1493
|
+
// Polling оставляем активным — это и есть «бг-чат».
|
|
1433
1494
|
},
|
|
1434
1495
|
// board.js зовёт после openFilm чтобы подгрузить chat-историю проекта.
|
|
1435
1496
|
loadFromCurrentProject: () => loadHistoryFromCurrentProject(),
|
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; }
|