kingkont 0.11.3 → 0.11.4

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.11.3",
3
+ "version": "0.11.4",
4
4
  "description": "KingKont · Chatium — нод-редактор сцен с AI-генерацией (картинки/видео/голос/SFX/музыка/текст)",
5
5
  "main": "main.js",
6
6
  "bin": {
package/renderer/board.js CHANGED
@@ -588,6 +588,7 @@ async function renderWelcomeRecents() {
588
588
  }
589
589
 
590
590
  // Карточка локального (папочного) проекта — извлечена из renderWelcomeRecents.
591
+ // Удаление и save-as-template — через ПКМ (по аналогии с cloud-карточкой).
591
592
  function makeRecentWelcomeCard(r) {
592
593
  const card = document.createElement('div');
593
594
  card.className = 'welcome-card';
@@ -609,17 +610,6 @@ function makeRecentWelcomeCard(r) {
609
610
  tsEl.textContent = fmtRelativeTime(r.ts);
610
611
  meta.append(nameEl, tsEl);
611
612
  card.append(thumb, meta);
612
- const del = document.createElement('div');
613
- del.className = 'welcome-card-del';
614
- del.textContent = '×';
615
- del.title = 'Удалить из недавних';
616
- del.addEventListener('click', async e => {
617
- e.stopPropagation();
618
- if (!confirm(`Убрать «${r.name}» из недавних? (папка не удаляется)`)) return;
619
- await removeRecent(r.name);
620
- await renderWelcomeRecents();
621
- });
622
- card.appendChild(del);
623
613
  card.addEventListener('click', async () => {
624
614
  try {
625
615
  if (r.handle) {
@@ -642,9 +632,50 @@ function makeRecentWelcomeCard(r) {
642
632
  alert('Ошибка: ' + (err?.message || err));
643
633
  }
644
634
  });
635
+ card.addEventListener('contextmenu', e => {
636
+ e.preventDefault();
637
+ e.stopPropagation();
638
+ showRecentCardContextMenu(r, e.clientX, e.clientY);
639
+ });
645
640
  return card;
646
641
  }
647
642
 
643
+ // ПКМ-меню для recent (folder) welcome-карточки. По аналогии с cloud:
644
+ // «Сохранить как шаблон» (открывает проект → saveCurrentProjectAsTemplate)
645
+ // + «Убрать из недавних».
646
+ function showRecentCardContextMenu(r, clientX, clientY) {
647
+ const menu = $('nodeMenu');
648
+ if (!menu) return;
649
+ menu.innerHTML = '';
650
+ const add = (label, fn, opts = {}) => {
651
+ const b = document.createElement('button');
652
+ b.textContent = label;
653
+ if (opts.danger) b.style.color = '#f88';
654
+ b.addEventListener('click', () => { menu.classList.add('hidden'); fn(); });
655
+ menu.appendChild(b);
656
+ };
657
+ add('💾 Сохранить как шаблон…', async () => {
658
+ if (!r.handle) { alert('Handle потерян, сначала открой проект и сохрани заново.'); return; }
659
+ try {
660
+ // Открываем сначала — saveCurrentProjectAsTemplate работает с filmHandle.
661
+ let g = (await r.handle.queryPermission({ mode: 'readwrite' })) === 'granted';
662
+ if (!g) g = (await r.handle.requestPermission({ mode: 'readwrite' })) === 'granted';
663
+ if (!g) { alert('Доступ к папке не подтверждён.'); return; }
664
+ await openFilm(r.handle);
665
+ if (typeof saveCurrentProjectAsTemplate === 'function') {
666
+ await saveCurrentProjectAsTemplate();
667
+ }
668
+ } catch (e) { alert('Ошибка: ' + (e?.message || e)); }
669
+ });
670
+ add('✖ Убрать из недавних', async () => {
671
+ if (!confirm(`Убрать «${r.name}» из недавних? (папка не удаляется)`)) return;
672
+ await removeRecent(r.name);
673
+ await renderWelcomeRecents();
674
+ }, { danger: true });
675
+ positionFloatingMenu(menu, clientX, clientY);
676
+ setTimeout(() => document.addEventListener('mousedown', closeNodeMenu, { once: true }), 0);
677
+ }
678
+
648
679
  // Карточка облачного проекта. Визуально такая же как у папочного, плюс
649
680
  // ☁ бейдж в углу обложки. Клик → cloudProjects.open() (использует local
650
681
  // cache если синхронизирован, иначе скачивает manifest+файлы).
@@ -715,6 +746,42 @@ function showCloudCardContextMenu(p, clientX, clientY) {
715
746
  add('↻ Обновить из облака', () => {
716
747
  if (window.cloudProjects?.open) window.cloudProjects.open(p.id, p.name, { forceRefresh: true });
717
748
  });
749
+ add('💾 Сохранить как шаблон…', async () => {
750
+ // Берём cloud-проект с сервера (manifest+files) и постим в /api/templates
751
+ // в template-формате (board.scene → board.manifest). Без открытия проекта.
752
+ const tplName = await askName('Имя шаблона:', '', p.name || '', { okText: 'Сохранить' });
753
+ if (!tplName) return;
754
+ try {
755
+ const r = await fetch('/api/projects/' + encodeURIComponent(p.id));
756
+ if (!r.ok) throw new Error('HTTP ' + r.status);
757
+ const proj = await r.json();
758
+ const tplBoards = (proj.manifest?.boards || []).map(b => ({
759
+ kind: b.kind, name: b.name,
760
+ manifest: b.scene || {}, // board.scene в cloud → board.manifest в template
761
+ files: b.files || {},
762
+ }));
763
+ const allFiles = {};
764
+ for (const b of tplBoards) {
765
+ for (const [rel, url] of Object.entries(b.files || {})) allFiles[`${b.kind}/${b.name}/${rel}`] = url;
766
+ }
767
+ const tR = await fetch('/api/templates', {
768
+ method: 'POST',
769
+ headers: { 'Content-Type': 'application/json' },
770
+ body: JSON.stringify({
771
+ name: tplName, kind: 'project',
772
+ manifest: { boards: tplBoards, coverUrl: proj.coverUrl || null },
773
+ files: allFiles,
774
+ }),
775
+ });
776
+ if (!tR.ok) {
777
+ const err = await tR.json().catch(() => ({}));
778
+ throw new Error(err.error || 'HTTP ' + tR.status);
779
+ }
780
+ alert(`Шаблон «${tplName}» создан.`);
781
+ } catch (e) {
782
+ alert('Не удалось сохранить шаблон: ' + (e?.message || e));
783
+ }
784
+ });
718
785
  add('🗑 Удалить с сервера', async () => {
719
786
  if (!confirm(`Удалить «${p.name}» с сервера? Локальная копия в userData останется.`)) return;
720
787
  try {
@@ -2315,27 +2382,11 @@ function showNodeContextMenu(node, clientX, clientY) {
2315
2382
  setTimeout(() => document.addEventListener('mousedown', closeNodeMenu, { once: true }), 0);
2316
2383
  }
2317
2384
 
2318
- // ПКМ на brand-area sidebar действия с проектом (сохранить как шаблон).
2319
- // Показывает «Создать новый шаблон» всегда, и «Обновить «X»» если проект
2320
- // был открыт из шаблона юзера (см. saveCurrentProjectAsTemplate — он сам
2321
- // определит mode по .kingkont-meta.json).
2385
+ // (ПКМ на brand-area внутри проекта раньше показывал «Сохранить как шаблон» —
2386
+ // убрано, переехало в ПКМ карточки на welcome-screen. Функция оставлена
2387
+ // для backward-compat если будут добавляться другие project-actions.)
2322
2388
  function showProjectContextMenu(clientX, clientY) {
2323
- const menu = $('nodeMenu');
2324
- menu.innerHTML = '';
2325
- const add = (label, fn) => {
2326
- const b = document.createElement('button');
2327
- b.textContent = label;
2328
- b.addEventListener('click', () => {
2329
- menu.classList.add('hidden');
2330
- fn();
2331
- });
2332
- menu.appendChild(b);
2333
- };
2334
- add('💾 Сохранить как шаблон…', () => {
2335
- if (typeof saveCurrentProjectAsTemplate === 'function') saveCurrentProjectAsTemplate();
2336
- });
2337
- positionFloatingMenu(menu, clientX, clientY);
2338
- setTimeout(() => document.addEventListener('mousedown', closeNodeMenu, { once: true }), 0);
2389
+ // intentional no-op: пока что внутри проекта ПКМ на brand ничего не делает.
2339
2390
  }
2340
2391
 
2341
2392
  // ПКМ на 📚 Templates-кнопке — действия проектного уровня.
package/renderer/chat.js CHANGED
@@ -337,7 +337,19 @@
337
337
  div.className = 'chat-msg chat-msg-' + m.role;
338
338
  const lbl = document.createElement('div');
339
339
  lbl.className = 'chat-msg-role';
340
- lbl.textContent = m.role === 'user' ? 'Вы' : 'Claude';
340
+ if (m.role === 'assistant') {
341
+ const img = document.createElement('img');
342
+ img.src = 'assets/icon.png';
343
+ img.className = 'chat-msg-avatar';
344
+ img.draggable = false;
345
+ img.alt = '';
346
+ lbl.appendChild(img);
347
+ const span = document.createElement('span');
348
+ span.textContent = 'KingKont';
349
+ lbl.appendChild(span);
350
+ } else {
351
+ lbl.textContent = 'Вы';
352
+ }
341
353
  div.appendChild(lbl);
342
354
  const body = document.createElement('div');
343
355
  body.className = 'chat-msg-body';
@@ -445,7 +457,7 @@
445
457
  history.push({ role: 'user', content: userText });
446
458
  renderHistory();
447
459
  persistDebounced();
448
- const status = appendStatus('Claude думает…');
460
+ const status = appendStatus('KingKont думает…');
449
461
  const system = buildSystemPrompt();
450
462
  try {
451
463
  let iter = 0;
@@ -530,7 +542,19 @@
530
542
  div.className = 'chat-msg chat-msg-' + m.role;
531
543
  const lbl = document.createElement('div');
532
544
  lbl.className = 'chat-msg-role';
533
- lbl.textContent = m.role === 'user' ? 'Вы' : 'Claude';
545
+ if (m.role === 'assistant') {
546
+ const img = document.createElement('img');
547
+ img.src = 'assets/icon.png';
548
+ img.className = 'chat-msg-avatar';
549
+ img.draggable = false;
550
+ img.alt = '';
551
+ lbl.appendChild(img);
552
+ const span = document.createElement('span');
553
+ span.textContent = 'KingKont';
554
+ lbl.appendChild(span);
555
+ } else {
556
+ lbl.textContent = 'Вы';
557
+ }
534
558
  div.appendChild(lbl);
535
559
  if (hasContent) {
536
560
  const body = document.createElement('div');
@@ -644,36 +644,8 @@
644
644
  $('newCloudProject')?.addEventListener('click', createNewCloudProject);
645
645
  $('openCloudProjects')?.addEventListener('click', openCloudProjectsModal);
646
646
  $('saveProjectCloud')?.addEventListener('click', saveCloudProject);
647
- // ПКМ на «☁ Сохранить на сервер» — расширенное меню: «Сохранить как
648
- // шаблон» (доступно ВСЕМ типам проектов, включая cloud — реализация
649
- // в templates.js работает через filmHandle, cloudFs-shim тоже подходит).
650
- $('saveProjectCloud')?.addEventListener('contextmenu', e => {
651
- e.preventDefault();
652
- e.stopPropagation();
653
- const menu = document.getElementById('nodeMenu');
654
- if (!menu) return;
655
- menu.innerHTML = '';
656
- const add = (label, fn) => {
657
- const b = document.createElement('button');
658
- b.textContent = label;
659
- b.addEventListener('click', () => { menu.classList.add('hidden'); fn(); });
660
- menu.appendChild(b);
661
- };
662
- add('☁ Сохранить на сервер', saveCloudProject);
663
- add('💾 Сохранить как шаблон…', () => {
664
- if (typeof saveCurrentProjectAsTemplate === 'function') saveCurrentProjectAsTemplate();
665
- else alert('saveCurrentProjectAsTemplate недоступен');
666
- });
667
- if (typeof positionFloatingMenu === 'function') {
668
- positionFloatingMenu(menu, e.clientX, e.clientY);
669
- } else {
670
- menu.style.cssText = `position:fixed; left:${e.clientX}px; top:${e.clientY}px; z-index:9999;`;
671
- }
672
- // Используем глобальный closeNodeMenu — он умеет НЕ закрываться на клик
673
- // по активной кнопке внутри меню (даёт click-обработчику сработать).
674
- // Без этого mousedown на кнопке закрывал меню до click → нажатие тонуло.
675
- setTimeout(() => document.addEventListener('mousedown', closeNodeMenu, { once: true }), 0);
676
- });
647
+ // (ПКМ на «☁ Сохранить на сервер» убран — «Сохранить как шаблон»
648
+ // переехал на welcome-screen в ПКМ карточки проекта.)
677
649
  setCloudButtonsVisibility();
678
650
  // Переинициализируем видимость кнопок раз в 5 сек — для случая когда юзер
679
651
  // login/logout в Chatium через настройки (нет других сигналов).
@@ -282,6 +282,11 @@
282
282
  .chat-msg-role {
283
283
  font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px;
284
284
  color: #888;
285
+ display: flex; align-items: center; gap: 6px;
286
+ }
287
+ .chat-msg-avatar {
288
+ width: 16px; height: 16px; border-radius: 4px;
289
+ object-fit: contain; background: #1f1f1f; padding: 1px;
285
290
  }
286
291
  .chat-msg-user .chat-msg-role { color: #6a9; }
287
292
  .chat-msg-assistant .chat-msg-role { color: #c97; }