kingkont 0.17.5 → 0.18.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/index.html CHANGED
@@ -610,6 +610,8 @@
610
610
  <!-- cloudProjects.js — после templates.js, переиспользует TPL_PROGRESS,
611
611
  walkBoardFiles, collectProjectBoards, uploadProjectCoverIfAny из templates.js. -->
612
612
  <script src="renderer/cloudProjects.js"></script>
613
+ <!-- autoNamer.js — LLM-based нейминг новых нод (общий util). -->
614
+ <script src="renderer/autoNamer.js"></script>
613
615
  <!-- chat.js — Claude-чат для управления сценой через tools (Cmd+J). -->
614
616
  <script src="renderer/chat.js"></script>
615
617
  <!-- notifyPanel.js — окно событий (генерации, чат-ответы, ошибки). 🔔 в углу. -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kingkont",
3
- "version": "0.17.5",
3
+ "version": "0.18.0",
4
4
  "description": "KingKont · Chatium — нод-редактор сцен с AI-генерацией (картинки/видео/голос/SFX/музыка/текст)",
5
5
  "main": "main.js",
6
6
  "bin": {
@@ -0,0 +1,64 @@
1
+ // renderer/autoNamer.js — async LLM-based нейминг новых нод.
2
+ //
3
+ // Когда юзер генерит ноду (image/video/audio/text), вместо стандартного
4
+ // `slugifyPrompt(prompt).slice(0, 30)` зовём LLM придумать имя в стиле
5
+ // существующих нод проекта. Возвращает строку (1-3 слова на русском),
6
+ // fallback на slugify если LLM недоступен.
7
+ //
8
+ // Дизайн:
9
+ // - Вызов через /api/text (server-side LLM proxy уже есть).
10
+ // - Не блокируем UI: вызов async, max ~2s.
11
+ // - Fallback: при ошибке/timeout возвращаем slugify-результат.
12
+ // - Дедуп: уникальность гарантируется в caller'е (uniqueNodeName).
13
+
14
+ (function () {
15
+ // Quick fallback на slugifyPrompt-like логику.
16
+ function _slugFallback(prompt) {
17
+ if (!prompt) return 'Нода';
18
+ const s = String(prompt).trim().split(/[\s.,!?:;]+/).slice(0, 3).join(' ');
19
+ return s.slice(0, 50) || 'Нода';
20
+ }
21
+
22
+ async function autoNameNode({ prompt, type, existingNames }) {
23
+ if (!prompt || prompt.length < 3) return _slugFallback(prompt);
24
+ const typeLabel = type === 'image' ? 'image' : type === 'video' ? 'video'
25
+ : type === 'audio' ? 'audio' : type === 'text' ? 'text' : 'node';
26
+ // Берём последние 30 — обычно стилистика однородная и за ~30 видна.
27
+ const sample = (existingNames || []).filter(Boolean).slice(-30);
28
+ const sys =
29
+ 'Ты придумываешь короткое имя для node в нод-редакторе. ' +
30
+ 'ПРАВИЛА: 1-3 слова, на русском, без кавычек, без пояснений. ' +
31
+ 'Должно отражать СУТЬ промпта/контента. Не повторяй существующие имена. ' +
32
+ 'Если в существующих есть стиль (например все имена — короткие глаголы или поэтичные эпитеты) — следуй ему.\n\n' +
33
+ `Существующие имена в проекте (${sample.length}): ${sample.length ? sample.map(n => `«${n}»`).join(', ') : '(нет)'}\n\n` +
34
+ `Тип ноды: ${typeLabel}\n` +
35
+ `Промпт/контент: ${String(prompt).slice(0, 600)}\n\n` +
36
+ 'Ответь ОДНОЙ СТРОКОЙ — только имя, ничего больше.';
37
+
38
+ try {
39
+ const r = await fetch('/api/text', {
40
+ method: 'POST',
41
+ headers: { 'Content-Type': 'application/json' },
42
+ body: JSON.stringify({
43
+ prompt: 'Придумай имя.',
44
+ system: sys,
45
+ // Используем самую быструю модель — имя короткое, надо моментально.
46
+ model: 'anthropic/claude-haiku-4.5',
47
+ }),
48
+ });
49
+ if (!r.ok) return _slugFallback(prompt);
50
+ const d = await r.json();
51
+ let name = (d?.text || '').trim();
52
+ // Убираем кавычки/lf/trailing-точки.
53
+ name = name.replace(/^["«]+|["»]+$/g, '').replace(/[\r\n].*/, '').trim();
54
+ // Защита от пустоты или огромных строк (LLM может выдать абзац).
55
+ if (!name || name.length > 60) return _slugFallback(prompt);
56
+ return name;
57
+ } catch (e) {
58
+ console.warn('[autoNamer] failed:', e?.message);
59
+ return _slugFallback(prompt);
60
+ }
61
+ }
62
+
63
+ window.autoNameNode = autoNameNode;
64
+ })();
package/renderer/chat.js CHANGED
@@ -179,6 +179,20 @@
179
179
  state.currentBoard.metadata.nodes.push(node);
180
180
  scheduleSave();
181
181
  if (typeof renderCanvas === 'function') await renderCanvas();
182
+ // Async auto-name если name не задан и есть prompt (LLM придумает
183
+ // имя в стиле существующих нод).
184
+ if (!node.name && prompt && typeof autoNameNode === 'function') {
185
+ const existing = state.currentBoard.metadata.nodes.map(n => n.name).filter(Boolean);
186
+ autoNameNode({ prompt, type, existingNames: existing }).then(autoName => {
187
+ if (!autoName) return;
188
+ const u = (typeof uniqueNodeName === 'function')
189
+ ? uniqueNodeName(state.currentBoard.metadata.nodes, autoName)
190
+ : autoName;
191
+ node.name = u;
192
+ scheduleSave();
193
+ if (typeof refreshNodeDOM === 'function') refreshNodeDOM(node);
194
+ }).catch(() => {});
195
+ }
182
196
  return { ok: true, id };
183
197
  },
184
198
  },
@@ -1297,6 +1311,24 @@
1297
1311
  } else if (statusEl && !statusEl.classList.contains('error')) {
1298
1312
  statusEl.remove();
1299
1313
  }
1314
+ // Бейдж «думает» на кнопке #chatBtn — видно даже когда панель скрыта.
1315
+ _updateChatBtnBadge(busy);
1316
+ }
1317
+
1318
+ // Маленький pulse-индикатор на 💬 Чат кнопке когда server-side loop идёт.
1319
+ // Юзер видит «работа в фоне» даже если панель закрыта или он на другой сцене.
1320
+ function _updateChatBtnBadge(busy) {
1321
+ const btn = document.getElementById('chatBtn');
1322
+ if (!btn) return;
1323
+ let dot = btn.querySelector('.chat-busy-dot');
1324
+ if (busy) {
1325
+ if (!dot) {
1326
+ dot = document.createElement('span');
1327
+ dot.className = 'chat-busy-dot';
1328
+ dot.style.cssText = 'display:inline-block; width:7px; height:7px; border-radius:50%; background:#fc9; margin-left:6px; animation:bgBadgePulse 1.2s ease-in-out infinite; vertical-align:middle;';
1329
+ btn.appendChild(dot);
1330
+ }
1331
+ } else if (dot) dot.remove();
1300
1332
  }
1301
1333
 
1302
1334
  async function send(userText) {
@@ -1621,6 +1653,7 @@
1621
1653
  resetInMemory: async () => {
1622
1654
  history = []; snapshots = [];
1623
1655
  renderHistory(); renderSnapshotBar();
1656
+ _updateChatBtnBadge(false); // dot прячется когда нет проекта
1624
1657
  // Polling оставляем активным — это и есть «бг-чат».
1625
1658
  },
1626
1659
  // board.js зовёт после openFilm чтобы подгрузить chat-историю проекта.
@@ -1646,6 +1646,20 @@ $('genSubmit').addEventListener('click', async () => {
1646
1646
  state.currentBoard.metadata.nodes.push(node);
1647
1647
  canvas.appendChild(await createNodeEl(node));
1648
1648
 
1649
+ // Async auto-name через LLM. НЕ блокируем — нода уже на холсте.
1650
+ // Когда LLM ответит — обновим node.name + DOM. Дедуп через uniqueNodeName.
1651
+ if (typeof autoNameNode === 'function' && !node.name) {
1652
+ const existing = state.currentBoard.metadata.nodes.map(n => n.name).filter(Boolean);
1653
+ autoNameNode({ prompt: rawPrompt || resolvedPrompt, type: kind, existingNames: existing }).then(name => {
1654
+ if (!name) return;
1655
+ const unique = uniqueNodeName(state.currentBoard.metadata.nodes, name);
1656
+ node.name = unique;
1657
+ scheduleSave();
1658
+ // Обновим title в node header (через refresh).
1659
+ if (typeof refreshNodeDOM === 'function') refreshNodeDOM(node);
1660
+ }).catch(() => {});
1661
+ }
1662
+
1649
1663
  // Если открывали через drag-line — фиксируем связь
1650
1664
  if (state.pendingConnectionFrom) {
1651
1665
  addConnection(state.pendingConnectionFrom, node.id);
@@ -2008,7 +2022,13 @@ async function pollJob(job, nodeId, bKey, boardHandle, kind) {
2008
2022
  const isCurrent = state.currentBoard?.key === bKey;
2009
2023
  if (typeof showToast === 'function') {
2010
2024
  const where = isCurrent ? '' : ` в ${bKey}`;
2011
- showToast(`✓ ${kind} «${nodeName || nodeId.slice(0,6)}» готов${where}`, isCurrent ? 'ok' : 'info');
2025
+ // target для click-навигации в notify-панели (откроется доска +
2026
+ // подсветится нода).
2027
+ showToast(
2028
+ `✓ ${kind} «${nodeName || nodeId.slice(0,6)}» готов${where}`,
2029
+ isCurrent ? 'ok' : 'info',
2030
+ { target: { projectKey: projectKeyAtCompletion, boardKey: bKey, nodeId } },
2031
+ );
2012
2032
  }
2013
2033
  // System notification — только когда генерация завершилась в фоне
2014
2034
  // (не на активной доске или когда окно скрыто). На активном UI
@@ -206,11 +206,12 @@
206
206
  // Init: ensure UI exists ASAP, hook into showToast и WS-events.
207
207
  document.addEventListener('DOMContentLoaded', () => {
208
208
  _ensureUI();
209
- // Wrap showToast чтобы дублировать в панель.
209
+ // Wrap showToast чтобы дублировать в панель + forward target для
210
+ // click-навигации (showToast(text, kind, {target: {...}})).
210
211
  if (typeof window.showToast === 'function') {
211
212
  const orig = window.showToast;
212
- window.showToast = function (text, kind) {
213
- try { addEvent({ kind: kind || 'info', text }); } catch {}
213
+ window.showToast = function (text, kind, opts) {
214
+ try { addEvent({ kind: kind || 'info', text, target: opts?.target || null }); } catch {}
214
215
  return orig.apply(this, arguments);
215
216
  };
216
217
  }
package/renderer/state.js CHANGED
@@ -201,12 +201,16 @@ window.systemNotify = systemNotify;
201
201
  // resume сработал, ...). Стек справа сверху. Auto-dismiss:
202
202
  // - ok/info — 10s (юзер должен заметить успех)
203
203
  // - error — 30s (важно, не пропустить)
204
- function showToast(text, kind) {
204
+ function showToast(text, kind, opts) {
205
+ // opts.target: {projectKey, boardKey, nodeId} — toast становится кликабельным.
205
206
  let host = document.getElementById('toastHost');
206
207
  if (!host) {
207
208
  host = document.createElement('div');
208
209
  host.id = 'toastHost';
209
- host.style.cssText = 'position:fixed; right:16px; top:64px; z-index:9999; display:flex; flex-direction:column; gap:8px; pointer-events:none; max-width:340px;';
210
+ // Bottom-left стек рядом с 🔔 кнопкой нотификаций. Новые toast'ы
211
+ // появляются СНИЗУ (flex-direction: column-reverse), так что они
212
+ // «всплывают» вверх от кнопки.
213
+ host.style.cssText = 'position:fixed; left:52px; bottom:12px; z-index:9999; display:flex; flex-direction:column-reverse; gap:8px; pointer-events:none; max-width:340px;';
210
214
  document.body.appendChild(host);
211
215
  }
212
216
  const t = document.createElement('div');
@@ -190,6 +190,10 @@
190
190
  overflow: hidden; /* recents скроллятся внутри своего блока */
191
191
  }
192
192
  body.no-project .sidebar, body.no-project .main, body.no-project .preview-panel { display: none !important; }
193
+ /* Preview-панель видна ТОЛЬКО когда таймлайн открыт. Закрыли таймлайн —
194
+ даже свернутая полоска preview прячется. Без этого юзер видит всегда
195
+ торчащий тёмный «остаток» справа, который мешает. */
196
+ body:has(.timeline-panel.hidden) .preview-panel { display: none !important; }
193
197
  .welcome-inner {
194
198
  display: flex; flex-direction: column; align-items: center; gap: 12px;
195
199
  width: 100%; max-width: none; padding: 0;
package/server.js CHANGED
@@ -356,9 +356,9 @@ async function handleChatClear(req, res) {
356
356
  async function handleJobsTrack(req, res) {
357
357
  try {
358
358
  const body = await readJson(req);
359
- const { action, projectKey, jobId, kind, name, type, taskId } = body || {};
359
+ const { action, projectKey, jobId, kind, name, type, taskId, boardKey } = body || {};
360
360
  if (!projectKey || !jobId) return send(res, 400, { error: 'projectKey + jobId обязательны' });
361
- if (action === 'start') jobsHub.start({ projectKey, jobId, kind, name, type, taskId });
361
+ if (action === 'start') jobsHub.start({ projectKey, jobId, kind, name, type, taskId, boardKey });
362
362
  else if (action === 'end') jobsHub.end({ projectKey, jobId });
363
363
  else return send(res, 400, { error: 'action: start|end' });
364
364
  send(res, 200, { ok: true });